xray-rails 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Brent Dillingham
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # Xray
2
+
3
+ ### Reveal the structure of your UI.
4
+
5
+ The dev tools available to web developers in modern browsers are great. Many of us can't remember what life was like before "Inspect Element". But what we see in the compiled output sent to our browser is often the wrong level of detail - what about being able to visualize the higher level components of your UI? Controllers, templates, partials, Backbone views, etc.
6
+
7
+ Xray is the missing link between the browser and your app code. Press **cmd+ctrl+x** to reveal an overlay of what files are powering your UI - click anything to open the associated file in your editor.
8
+
9
+ ![Screenshot](http://dl.dropbox.com/u/156655/Screenshots/xgf7ukh3fya-.png)
10
+
11
+ ### Current Support
12
+
13
+ Xray is in early stages and currently supports only Rails 3.1+ with use of the asset pipeline as a requirement.
14
+
15
+ So far, Xray can reveal:
16
+
17
+ * Rails views and partials
18
+ * Backbone View instances
19
+
20
+ ### Installation
21
+
22
+ Add to your Gemfile, preferably under your development group:
23
+
24
+ ```ruby
25
+ group :development do
26
+ ...
27
+ gem 'xray-rails', github: 'brentd/xray-rails'
28
+ end
29
+ ```
30
+
31
+ Then bundle and delete your cached assets:
32
+
33
+ ```
34
+ $ bundle && rm -rf tmp/cache/assets
35
+ ```
36
+
37
+ Restart your app, open your browser, and press `cmd+ctrl+x` to see the overlay.
38
+
39
+ ### Configuration
40
+
41
+ By default, Xray will open files with Sublime Text, looking for `/usr/local/bin/subl`.
42
+
43
+ You can configure this to be your editor of choice in Xray's UI, or create `~/.xrayconfig`, a YAML file.
44
+
45
+ Example `.xrayconfig`:
46
+
47
+ ```yaml
48
+ :editor: '/usr/local/bin/mate'
49
+ ```
50
+
51
+ Or for something more complex, use the `$file` placeholder.
52
+
53
+ ```yaml
54
+ :editor: "/usr/local/bin/tmux split-window -v '/usr/local/bin/vim $file'"
55
+ ```
56
+
57
+ ### How this works
58
+
59
+ * During asset compilation, JS files and templates are modified to contain file path information.
60
+ * A middleware inserts `xray.js`, `xray.css`, and the Xray bar into all successful HTML response bodies.
61
+ * When the overlay is shown, `xray.js` examines the file path information inserted during asset compilation.
62
+ * Another middleware piggybacks the Rails server to respond to requests to open file paths with the user's desired editor.
63
+
64
+ ### Contributing
65
+
66
+ If you have an idea, open an issue and let's talk about it, or fork away and send a pull request.
67
+
68
+ A laundry list of things to take on:
69
+
70
+ * Reveal views from Ember, Knockout, Angular, etc.
71
+ * Overlapping boxes are a problem - parent views in real applications will often be obscured by their children.
72
+ * The current scheme for associating a JS constructor with a filepath is messy and can make stack traces ugly.
73
+
74
+ Worth noting is that I have plans to solidify xray.js into an API and specification that could be used to aid development in any framework - not just Rails and the asset pipeline.
@@ -0,0 +1,20 @@
1
+ # Xray Backbone integration. This involves hooking into the lifecycle
2
+ # of Backbone.View by monkey patching its prototype. Would love a cleaner
3
+ # way of doing this, as nobody wants to this stuff in their stack traces.
4
+
5
+ return unless window.Backbone && window.Xray
6
+
7
+ # Wrap Backbone.View::_ensureElement to add the view to Xray once
8
+ # its element has been setup.
9
+ _ensureElement = Backbone.View::_ensureElement
10
+ Backbone.View::_ensureElement = ->
11
+ _.defer =>
12
+ info = Xray.constructorInfo @constructor
13
+ Xray.ViewSpecimen.add @el, info
14
+ _ensureElement.apply(this, arguments)
15
+
16
+ # Cleanup when view is removed.
17
+ _remove = Backbone.View::remove
18
+ Backbone.View::remove = ->
19
+ Xray.ViewSpecimen.remove @el
20
+ _remove.apply(this, arguments)
@@ -0,0 +1,282 @@
1
+ window.Xray = {}
2
+ return unless $ = window.jQuery
3
+
4
+ # Max CSS z-index. The overlay and xray bar use this.
5
+ MAX_ZINDEX = 2147483647
6
+
7
+ # Initialize Xray. Called immediately, but some setup is deferred until DOM ready.
8
+ Xray.init = do ->
9
+ return if Xray.initialized
10
+ Xray.initialized = true
11
+
12
+ # Register keyboard shortcuts
13
+ $(document).keydown (e) ->
14
+ if e.ctrlKey and e.metaKey and e.keyCode is 88 # cmd+ctrl+x
15
+ if Xray.isShowing then Xray.hide() else Xray.show()
16
+ if Xray.isShowing and e.keyCode is 27 # esc
17
+ Xray.hide()
18
+
19
+ $ ->
20
+ # Instantiate the overlay singleton.
21
+ new Xray.Overlay
22
+ # Go ahead and do a pass on the DOM to find templates.
23
+ Xray.findTemplates()
24
+ # Ready to rock.
25
+ console?.log "Ready to Xray. Press cmd+ctrl+x to scan your UI."
26
+
27
+ # Returns all currently created Xray.Specimen objects.
28
+ Xray.specimens = ->
29
+ Xray.ViewSpecimen.all.concat Xray.TemplateSpecimen.all
30
+
31
+ # Looks up the stored constructor info
32
+ Xray.constructorInfo = (constructor) ->
33
+ if window.XrayPaths
34
+ for own info, func of window.XrayPaths
35
+ return JSON.parse(info) if func == constructor
36
+ null
37
+
38
+ # Scans the document for templates, creating Xray.TemplateSpecimens for them.
39
+ Xray.findTemplates = -> util.bm 'findTemplates', ->
40
+ # Find all <!-- XRAY START ... --> comments
41
+ comments = $('*:not(iframe,script)').contents().filter ->
42
+ this.nodeType == 8 and this.data[0..9] == "XRAY START"
43
+
44
+ # Find the <!-- XRAY END ... --> comment for each. Everything between the
45
+ # start and end comment becomes the contents of an Xray.TemplateSpecimen.
46
+ for comment in comments
47
+ [_, id, path] = comment.data.match(/^XRAY START (\d+) (.*)$/)
48
+ $templateContents = new jQuery
49
+ el = comment.nextSibling
50
+ until !el or (el.nodeType == 8 and el.data == "XRAY END #{id}")
51
+ if el.nodeType == 1 and el.tagName != 'SCRIPT'
52
+ $templateContents.push el
53
+ el = el.nextSibling
54
+ # Remove XRAY template comments from the DOM.
55
+ el.parentNode.removeChild(el) if el?.nodeType == 8
56
+ comment.parentNode.removeChild(comment)
57
+ # Add the template specimen
58
+ Xray.TemplateSpecimen.add $templateContents,
59
+ name: path.split('/').slice(-1)[0]
60
+ path: path
61
+
62
+ # Open the given filesystem path by calling out to Xray's server.
63
+ Xray.open = (path) ->
64
+ $.ajax(url: "/_xray/open?path=#{path}")
65
+
66
+ # Show the Xray overlay
67
+ Xray.show = (type = null) ->
68
+ Xray.Overlay.instance().show(type)
69
+
70
+ # Hide the Xray overlay
71
+ Xray.hide = ->
72
+ Xray.Overlay.instance().hide()
73
+
74
+ Xray.toggleSettings = ->
75
+ Xray.Overlay.instance().settings.toggle()
76
+
77
+ # Wraps a DOM element that Xray is tracking. This is subclassed by
78
+ # Xray.TemplateSpecimen and Xray.ViewSpecimen.
79
+ class Xray.Specimen
80
+ @add: (el, info = {}) ->
81
+ @all.push new this(el, info)
82
+
83
+ @remove: (el) ->
84
+ @find(el)?.remove()
85
+
86
+ @find: (el) ->
87
+ el = el[0] if el instanceof jQuery
88
+ for specimen in @all
89
+ return specimen if specimen.el == el
90
+ null
91
+
92
+ @reset: ->
93
+ @all = []
94
+
95
+ constructor: (contents, info = {}) ->
96
+ @el = if contents instanceof jQuery then contents[0] else contents
97
+ @$contents = $(contents)
98
+ @name = info.name
99
+ @path = info.path
100
+
101
+ remove: ->
102
+ idx = @constructor.all.indexOf(this)
103
+ @constructor.all.splice(idx, 1) unless idx == -1
104
+
105
+ isVisible: ->
106
+ @$contents.length and @$contents.is(':visible')
107
+
108
+ makeBox: ->
109
+ @bounds = util.computeBoundingBox(@$contents)
110
+ @$box = $("<div class='xray-specimen #{@constructor.name}'>").css(@bounds)
111
+
112
+ # If the element is fixed, override the computed position with the fixed one.
113
+ if @$contents.css('position') == 'fixed'
114
+ @$box.css
115
+ position : 'fixed'
116
+ top : @$contents.css('top')
117
+ left : @$contents.css('left')
118
+
119
+ @$box.click => Xray.open @path
120
+ @$box.append @makeLabel
121
+
122
+ makeLabel: =>
123
+ $("<div class='xray-specimen-handle #{@constructor.name}'>").append(@name)
124
+
125
+
126
+ # Wraps elements that constitute a Javascript "view" object, e.g.
127
+ # Backbone.View.
128
+ class Xray.ViewSpecimen extends Xray.Specimen
129
+ @all = []
130
+
131
+
132
+ # Wraps elements that were rendered by a template, e.g. a Rails partial or
133
+ # a client-side rendered JS template.
134
+ class Xray.TemplateSpecimen extends Xray.Specimen
135
+ @all = []
136
+
137
+
138
+ # Singleton class for the Xray "overlay" invoked by the keyboard shortcut
139
+ class Xray.Overlay
140
+ @instance: ->
141
+ @singletonInstance ||= new this
142
+
143
+ constructor: ->
144
+ Xray.Overlay.singletonInstance = this
145
+ @bar = new Xray.Bar('#xray-bar')
146
+ @settings = new Xray.Settings('#xray-settings')
147
+ @shownBoxes = []
148
+ @$overlay = $('<div id="xray-overlay">')
149
+ @$overlay.click => @hide()
150
+
151
+ show: (type = null) ->
152
+ @reset()
153
+ Xray.isShowing = true
154
+ util.bm 'show', =>
155
+ @bar.$el.find('#xray-bar-togglers .xray-bar-btn').removeClass('active')
156
+ unless @$overlay.is(':visible')
157
+ $('body').append @$overlay
158
+ @bar.show()
159
+ switch type
160
+ when 'templates'
161
+ Xray.findTemplates()
162
+ specimens = Xray.TemplateSpecimen.all
163
+ @bar.$el.find('.xray-bar-templates-toggler').addClass('active')
164
+ when 'views'
165
+ specimens = Xray.ViewSpecimen.all
166
+ @bar.$el.find('.xray-bar-views-toggler').addClass('active')
167
+ else
168
+ Xray.findTemplates()
169
+ specimens = Xray.specimens()
170
+ @bar.$el.find('.xray-bar-all-toggler').addClass('active')
171
+ for element in specimens
172
+ continue unless element.isVisible()
173
+ element.makeBox()
174
+ # A cheap way to "order" the boxes, where boxes positioned closer to the
175
+ # bottom right of the document have a higher z-index.
176
+ element.$box.css
177
+ zIndex: Math.ceil(MAX_ZINDEX*0.9 + element.bounds.top + element.bounds.left)
178
+ @shownBoxes.push element.$box
179
+ $('body').append element.$box
180
+
181
+ reset: ->
182
+ $box.remove() for $box in @shownBoxes
183
+ @shownBoxes = []
184
+
185
+ hide: ->
186
+ Xray.isShowing = false
187
+ @$overlay.detach()
188
+ @reset()
189
+ @bar.hide()
190
+
191
+
192
+ # The Xray bar shows controller, action, and view information, and has
193
+ # toggle buttons for showing the different types of specimens in the overlay.
194
+ class Xray.Bar
195
+ constructor: (el) ->
196
+ @$el = $(el)
197
+ @$el.css(zIndex: MAX_ZINDEX)
198
+ @$el.find('#xray-bar-controller-path .xray-bar-btn').click ->
199
+ Xray.open($(this).attr('data-path'))
200
+ @$el.find('.xray-bar-all-toggler').click -> Xray.show()
201
+ @$el.find('.xray-bar-templates-toggler').click -> Xray.show('templates')
202
+ @$el.find('.xray-bar-views-toggler').click -> Xray.show('views')
203
+ @$el.find('.xray-bar-settings-btn').click -> Xray.toggleSettings()
204
+
205
+ show: ->
206
+ @$el.show()
207
+ @originalPadding = parseInt $('html').css('padding-bottom')
208
+ if @originalPadding < 40
209
+ $('html').css paddingBottom: 40
210
+
211
+ hide: ->
212
+ @$el.hide()
213
+ $('html').css paddingBottom: @originalPadding
214
+
215
+
216
+ class Xray.Settings
217
+ constructor: (el) ->
218
+ @$el = $(el)
219
+ @$el.find('form').submit @save
220
+
221
+ toggle: =>
222
+ @$el.toggle()
223
+
224
+ save: (e) =>
225
+ e.preventDefault()
226
+ editor = @$el.find('#xray-editor-input').val()
227
+ $.ajax
228
+ url: '/_xray/config'
229
+ type: 'POST'
230
+ data: {editor: editor}
231
+ success: => @displayUpdateMsg(true)
232
+ error: => @displayUpdateMsg(false)
233
+
234
+ displayUpdateMsg: (success) =>
235
+ if success
236
+ $msg = $("<span class='xray-settings-success xray-settings-update-msg'>Success!</span>")
237
+ else
238
+ $msg = $("<span class='xray-settings-error xray-settings-update-msg'>Uh oh, something went wrong!</span>")
239
+ @$el.append($msg)
240
+ $msg.delay(2000).fadeOut(500, => $msg.remove(); @toggle())
241
+
242
+
243
+ # Utility methods.
244
+ util =
245
+ # Benchmark a piece of code
246
+ bm: (name, fn) ->
247
+ time = new Date
248
+ result = fn()
249
+ # console.log "#{name} : #{new Date() - time}ms"
250
+ result
251
+
252
+ # Computes the bounding box of a jQuery set, which may be many sibling
253
+ # elements with no parent in the set.
254
+ computeBoundingBox: ($contents) ->
255
+ # Edge case: the container may not physically wrap its children, for
256
+ # example if they are floated and no clearfix is present.
257
+ if $contents.length == 1 and $contents.height() <= 0
258
+ return util.computeBoundingBox($contents.children())
259
+
260
+ boxFrame =
261
+ top : Number.POSITIVE_INFINITY
262
+ left : Number.POSITIVE_INFINITY
263
+ right : Number.NEGATIVE_INFINITY
264
+ bottom : Number.NEGATIVE_INFINITY
265
+
266
+ for el in $contents
267
+ $el = $(el)
268
+ continue unless $el.is(':visible')
269
+ frame = $el.offset()
270
+ frame.right = frame.left + $el.outerWidth()
271
+ frame.bottom = frame.top + $el.outerHeight()
272
+ boxFrame.top = frame.top if frame.top < boxFrame.top
273
+ boxFrame.left = frame.left if frame.left < boxFrame.left
274
+ boxFrame.right = frame.right if frame.right > boxFrame.right
275
+ boxFrame.bottom = frame.bottom if frame.bottom > boxFrame.bottom
276
+
277
+ return {
278
+ left : boxFrame.left
279
+ top : boxFrame.top
280
+ width : boxFrame.right - boxFrame.left
281
+ height : boxFrame.bottom - boxFrame.top
282
+ }
@@ -0,0 +1,349 @@
1
+ @charset "UTF-8";
2
+
3
+ /* selector for element and children */
4
+ #xray-overlay, #xray-overlay *, #xray-overlay a:hover, #xray-overlay a:visited, #xray-overlay a:active,
5
+ #xray-bar, #xray-bar *, #xray-bar a:hover, #xray-bar a:visited, #xray-bar a:active {
6
+ background:none;
7
+ border:none;
8
+ bottom:auto;
9
+ clear:none;
10
+ cursor:default;
11
+ float:none;
12
+ font-family:Arial, Helvetica, sans-serif;
13
+ font-size:medium;
14
+ font-style:normal;
15
+ font-weight:normal;
16
+ height:auto;
17
+ left:auto;
18
+ letter-spacing:normal;
19
+ line-height:normal;
20
+ max-height:none;
21
+ max-width:none;
22
+ min-height:0;
23
+ min-width:0;
24
+ overflow:visible;
25
+ position:static;
26
+ right:auto;
27
+ text-align:left;
28
+ text-decoration:none;
29
+ text-indent:0;
30
+ text-transform:none;
31
+ top:auto;
32
+ visibility:visible;
33
+ white-space:normal;
34
+ width:auto;
35
+ z-index:auto;
36
+ }
37
+
38
+ #xray-overlay {
39
+ position: fixed; left: 0; top: 0; bottom: 0; right: 0;
40
+ background: rgba(0,0,0,0.7);
41
+ background: -webkit-radial-gradient(center, ellipse cover, rgba(0,0,0,0.4) 10%, rgba(0,0,0,0.8) 100%);
42
+ z-index: 9000;
43
+ }
44
+
45
+ .xray-specimen {
46
+ position: absolute;
47
+ background: rgba(255,255,255,0.15);
48
+ outline: 1px solid rgba(255,255,255,0.8);
49
+ outline-offset: -1px;
50
+ color: #666;
51
+ font-family: "Helvetica Neue", sans-serif;
52
+ font-size: 13px;
53
+ box-shadow: 0 1px 3px rgba(0,0,0,0.7);
54
+ }
55
+
56
+ .xray-specimen:hover {
57
+ cursor: pointer;
58
+ background: rgba(255,255,255,0.4);
59
+ }
60
+
61
+ .xray-specimen.TemplateSpecimen {
62
+ outline: 1px solid rgba(255,50,50,0.8);
63
+ background: rgba(255,50,50,0.1);
64
+ }
65
+
66
+ .xray-specimen.TemplateSpecimen:hover {
67
+ background: rgba(255,50,50,0.4);
68
+ }
69
+
70
+ .xray-specimen-handle {
71
+ float:left;
72
+ background: #fff;
73
+ padding: 0 3px;
74
+ color: #333;
75
+ font-size: 10px;
76
+ }
77
+
78
+ .xray-specimen-handle.TemplateSpecimen {
79
+ background: rgba(255,50,50,0.8);
80
+ color: #fff;
81
+ }
82
+
83
+ #xray-bar {
84
+ position: fixed;
85
+ left: 0;
86
+ right: 0;
87
+ bottom: 0;
88
+ height: 40px;
89
+ padding: 0 8px;
90
+ background: #222;
91
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
92
+ font-weight: 200;
93
+ color: #fff;
94
+ z-index: 10000;
95
+ box-shadow: 0 -1px 0 rgba(255,255,255,0.1), inset 0 2px 6px rgba(0,0,0,0.8);
96
+ background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3)),
97
+ url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDRkNBMTUwNzdGRTIxMUUyQjBGQ0NBRTc5RDQ3MEJFNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDRkNBMTUwODdGRTIxMUUyQjBGQ0NBRTc5RDQ3MEJFNSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkNGQ0ExNTA1N0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkNGQ0ExNTA2N0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+aIv7XwAAAEVJREFUeNpiFBMTY2BgEBISYsAGWICAATdgkZeXB1Lv37/HLo1LAgKYGPACdOl3YIAwHNOpKFw0aTSXokujuZSA0wACDABh2BIyJ1wQkwAAAABJRU5ErkJggg==);
98
+ }
99
+
100
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
101
+ #xray-bar {
102
+ background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3)),
103
+ url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0Q0ZGQkRGRTdGRTMxMUUyQjBGQ0NBRTc5RDQ3MEJFNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0Q0ZGQkRGRjdGRTMxMUUyQjBGQ0NBRTc5RDQ3MEJFNSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkNGQ0ExNTA5N0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkNGQ0ExNTBBN0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+T6y4zwAAAIZJREFUeNrkkjEKwCAMRWMbERw8gE5O3v9q6lIEbbDQMZJ2Kv0gBOSR8HkqxphzBgDnnDEGJNkRsfc+xmitWWtF8KaUuqZ7EMDee1qutQ4hSGGkl1Kis2utYviYgUfZ4EU+CiP/TV0y/i02l1L6DA3is3n/FjDvHy5bYfxbF8b490fDTgEGAJveOCvuYEabAAAAAElFTkSuQmCC);
104
+ background-size: auto, 10px 10px;
105
+ }
106
+ }
107
+
108
+ #xray-bar .xray-bar-btn {
109
+ position: relative;
110
+ color: #fff;
111
+ margin: 8px 1px;
112
+ height: 24px;
113
+ line-height: 24px;
114
+ padding: 0 8px;
115
+ float: left;
116
+ font-size: 14px;
117
+ cursor: pointer;
118
+ vertical-align: middle;
119
+ background-color: #444;
120
+ background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.2));
121
+ border-radius: 2px;
122
+ box-shadow: 1px 1px 1px rgba(0,0,0,0.5),
123
+ inset 0 1px 0 rgba(255, 255, 255, 0.2),
124
+ inset 0 0 2px rgba(255, 255, 255, 0.2);
125
+ text-shadow: 0 -1px 0 rgba(0,0,0,0.4);
126
+ transition: background-color 0.1s;
127
+ }
128
+
129
+ #xray-bar .xray-bar-btn b {
130
+ position: absolute;
131
+ display: block;
132
+ right: -19px;
133
+ top: 0;
134
+ width: 20px;
135
+ height: 24px;
136
+ z-index: 10;
137
+ overflow: hidden;
138
+ font-size: 44px;
139
+ line-height: 19px;
140
+ text-indent: -7px;
141
+ }
142
+
143
+ #xray-bar .xray-bar-btn b:before {
144
+ content: "";
145
+ width: 18px;
146
+ height: 18px;
147
+ display: block;
148
+ position: absolute;
149
+ left: -9px;
150
+ top: 3px;
151
+ border-radius: 2px;
152
+ box-shadow: 1px -1px 1px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 2px rgba(255, 255, 255, 0.2);
153
+ background-image: linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0.2));
154
+ -webkit-transform: rotate(45deg);
155
+ transform: rotate(45deg);
156
+ transition: background-color 0.1s;
157
+ }
158
+
159
+ #xray-bar .xray-bar-btn:hover {
160
+ background-color: #555;
161
+ }
162
+
163
+ #xray-bar #xray-bar-controller-path {
164
+ margin-right: 20px;
165
+ }
166
+
167
+ #xray-bar #xray-bar-controller-path .xray-bar-btn {
168
+ border-radius: 2px 0 0 2px;
169
+ padding: 0 6px 0 16px;
170
+ margin: 8px 1px 8px 0;
171
+ }
172
+
173
+ #xray-bar #xray-bar-controller-path .xray-bar-btn:first-child {
174
+ padding-left: 8px;
175
+ }
176
+
177
+ #xray-bar #xray-bar-controller-path .xray-bar-btn:last-child {
178
+ border-radius: 2px;
179
+ padding-right: 10px;
180
+ }
181
+
182
+ #xray-bar-controller-path .xray-bar-controller { padding-left: 6px; }
183
+ #xray-bar-controller-path .xray-bar-controller,
184
+ #xray-bar-controller-path .xray-bar-controller b:before { background-color: #444; }
185
+ #xray-bar-controller-path .xray-bar-controller:hover,
186
+ #xray-bar-controller-path .xray-bar-controller:hover b:before { background-color: #555; }
187
+ #xray-bar-controller-path .xray-bar-controller-action { color: #ddd; }
188
+ #xray-bar-controller-path .xray-bar-layout,
189
+ #xray-bar-controller-path .xray-bar-layout b:before { background-color: #c12e27; }
190
+ #xray-bar-controller-path .xray-bar-layout:hover,
191
+ #xray-bar-controller-path .xray-bar-layout:hover b:before { background-color: #de362d; }
192
+ #xray-bar-controller-path .xray-bar-view,
193
+ #xray-bar-controller-path .xray-bar-view b:before { background-color: #ff2c1e; }
194
+ #xray-bar-controller-path .xray-bar-view:hover,
195
+ #xray-bar-controller-path .xray-bar-view:hover b:before { background-color: #ff4c36; }
196
+
197
+ #xray-bar #xray-bar-togglers {
198
+ float: left;
199
+ margin-left: 20px;
200
+ }
201
+
202
+ #xray-bar #xray-bar-togglers .xray-bar-btn {
203
+ border-radius: 0;
204
+ margin-right: 0;
205
+ color: #999;
206
+ }
207
+
208
+ #xray-bar #xray-bar-togglers .xray-bar-btn:first-child {
209
+ border-radius: 2px 0 0 2px;
210
+ }
211
+
212
+ #xray-bar #xray-bar-togglers .xray-bar-btn:last-child {
213
+ border-radius: 0 2px 2px 0;
214
+ }
215
+
216
+ #xray-bar #xray-bar-togglers .xray-bar-btn:before {
217
+ font-size: 9px;
218
+ vertical-align: middle;
219
+ margin-bottom: 1px;
220
+ margin-right: 5px;
221
+ background: rgba(255,255,255,0.2);
222
+ color: #eee;
223
+ padding: 2px 4px;
224
+ text-shadow: none;
225
+ }
226
+
227
+ #xray-bar #xray-bar-togglers .xray-bar-btn.active {
228
+ background: #555;
229
+ color: #fff;
230
+ }
231
+
232
+ #xray-bar #xray-bar-togglers .xray-bar-templates-toggler:before { content: 'HTML'; }
233
+ #xray-bar #xray-bar-togglers .xray-bar-templates-toggler.active:before { background: red; }
234
+ #xray-bar #xray-bar-togglers .xray-bar-views-toggler:before { content: 'JS'; }
235
+ #xray-bar #xray-bar-togglers .xray-bar-views-toggler.active:before { background: #fff; color: #333; }
236
+ #xray-bar #xray-bar-togglers .xray-bar-styles-toggler:before { content: 'CSS'; }
237
+
238
+ #xray-bar #xray-bar-togglers .xray-icon-search:before {
239
+ font-size: 16px;
240
+ background: none;
241
+ padding: 0;
242
+ margin: 0;
243
+ }
244
+
245
+ #xray-bar .xray-bar-settings-btn {
246
+ position: absolute;
247
+ right: 10px;
248
+ top: 10px;
249
+ color: #666;
250
+ cursor: pointer;
251
+ text-shadow: 0 1px 0 #000;
252
+ font-size: 16px;
253
+ -webkit-touch-callout: none;
254
+ -webkit-user-select: none;
255
+ -khtml-user-select: none;
256
+ -moz-user-select: none;
257
+ -ms-user-select: none;
258
+ user-select: none;
259
+ }
260
+
261
+ #xray-bar .xray-bar-settings-btn:hover {
262
+ color: #fff;
263
+ }
264
+
265
+ #xray-settings {
266
+ position: absolute;
267
+ right: 0;
268
+ bottom: 40px;
269
+ width: 300px;
270
+ height: 100px;
271
+ background: rgba(0,0,0,0.9);
272
+ padding: 10px;
273
+ font-size: 14px
274
+ }
275
+
276
+ #xray-settings label {
277
+ display: inline;
278
+ margin: 2px 10px;
279
+ padding: 0;
280
+ }
281
+
282
+ #xray-settings input {
283
+ padding: 5px;
284
+ display: inline;
285
+ background: #333;
286
+ border: 1px solid #666;
287
+ color: #fff;
288
+ width: 200px;
289
+ margin: 0;
290
+ font-size: 13px;
291
+ line-height: 13px;
292
+ border-radius: 3px;
293
+ vertical-align: middle;
294
+ }
295
+
296
+ #xray-settings button {
297
+ position: absolute;
298
+ right: 18px;
299
+ left: 18px;
300
+ bottom: 10px;
301
+ padding: 7px;
302
+ color: #fff;
303
+ background: #04be00;
304
+ text-align: center;
305
+ cursor: pointer;
306
+ }
307
+
308
+ #xray-settings button:hover {
309
+ background: #049d00;
310
+ }
311
+
312
+ #xray-settings p {
313
+ font-size: 12px;
314
+ color: #666;
315
+ text-align: center;
316
+ margin: 10px 0 0 0;
317
+ }
318
+
319
+ #xray-settings .xray-settings-update-msg {
320
+ margin-left: 12px;
321
+ }
322
+
323
+
324
+ @font-face {
325
+ font-family: 'xray-icons';
326
+ src: url("data:application/octet-stream;base64,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") format('woff'), url("data:application/octet-stream;base64,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JWBkI7ADJWBkUFghGyMhWSMgILAEJiNGYThZLbAwLLAAFiAgILAFJiAuRyNHI2EjPDgtsDEssAAWILAJI0IgICBGI0ewACsjYTgtsDIssAAWsAMlsAIlRyNHI2GwAFRYLiA8IyEbsAIlsAIlRyNHI2EgsAUlsAQlRyNHI2GwBiWwBSVJsAIlYbABRWMjIFhiGyFZY7ABRWJgIy4jICA8ijgjIVktsDMssAAWILAJQyAuRyNHI2EgYLAgYGawgGIjICA8ijgtsDQsIyAuRrACJUZSWCA8WS6xJAEUKy2wNSwjIC5GsAIlRlBYIDxZLrEkARQrLbA2LCMgLkawAiVGUlggPFkjIC5GsAIlRlBYIDxZLrEkARQrLbA3LLAuKyMgLkawAiVGUlggPFkusSQBFCstsDgssC8riiAgPLAEI0KKOCMgLkawAiVGUlggPFkusSQBFCuwBEMusCQrLbA5LLAAFrAEJbAEJiAuRyNHI2GwBkUrIyA8IC4jOLEkARQrLbA6LLEJBCVCsAAWsAQlsAQlIC5HI0cjYSCwBCNCsAZFKyCwYFBYILBAUVizAiADIBuzAiYDGllCQiMgR7AEQ7CAYmAgsAArIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbCAYmGwAiVGYTgjIDwjOBshICBGI0ewACsjYTghWbEkARQrLbA7LLAuKy6xJAEUKy2wPCywLyshIyAgPLAEI0IjOLEkARQrsARDLrAkKy2wPSywABUgR7AAI0KyAAEBFRQTLrAqKi2wPiywABUgR7AAI0KyAAEBFRQTLrAqKi2wPyyxAAEUE7ArKi2wQCywLSotsEEssAAWRSMgLiBGiiNhOLEkARQrLbBCLLAJI0KwQSstsEMssgAAOistsEQssgABOistsEUssgEAOistsEYssgEBOistsEcssgAAOystsEgssgABOystsEkssgEAOystsEossgEBOystsEsssgAANystsEwssgABNystsE0ssgEANystsE4ssgEBNystsE8ssgAAOSstsFAssgABOSstsFEssgEAOSstsFIssgEBOSstsFMssgAAPCstsFQssgABPCstsFUssgEAPCstsFYssgEBPCstsFcssgAAOCstsFgssgABOCstsFkssgEAOCstsFossgEBOCstsFsssDArLrEkARQrLbBcLLAwK7A0Ky2wXSywMCuwNSstsF4ssAAWsDArsDYrLbBfLLAxKy6xJAEUKy2wYCywMSuwNCstsGEssDErsDUrLbBiLLAxK7A2Ky2wYyywMisusSQBFCstsGQssDIrsDQrLbBlLLAyK7A1Ky2wZiywMiuwNistsGcssDMrLrEkARQrLbBoLLAzK7A0Ky2waSywMyuwNSstsGossDMrsDYrLbBrLCuwCGWwAyRQeLABFTAtAABLuADIUlixAQGOWbkIAAgAYyCwASNEILADI3CyBCgJRVJEswoLBgQrsQYBRLEkAYhRWLBAiFixBgNEsSYBiFFYuAQAiFixBgFEWVlZWbgB/4WwBI2xBQBE") format('truetype');
327
+ }
328
+
329
+ [class^="xray-icon-"]:before,
330
+ [class*=" xray-icon-"]:before {
331
+ font-family: 'xray-icons';
332
+ font-style: normal;
333
+ font-weight: normal;
334
+ speak: none;
335
+ display: inline-block;
336
+ text-decoration: inherit;
337
+ width: 1em;
338
+ margin-right: 0.1em;
339
+ text-align: center;
340
+ line-height: 1em;
341
+ }
342
+
343
+ .xray-icon-cog:before { content: '\e800'; } /* '' */
344
+ .xray-icon-flash:before { content: '\26a1'; } /* '⚡' */
345
+ .xray-icon-cancel:before { content: '\2715'; } /* '✕' */
346
+ .xray-icon-github:before { content: '\f056'; } /* '' */
347
+ .xray-icon-columns:before { content: '\f0db'; } /* '' */
348
+ .xray-icon-doc:before { content: '📄'; } /* '\1f4c4' */
349
+ .xray-icon-search:before { content: '🔍'; } /* '\1f50d' */
@@ -0,0 +1,46 @@
1
+
2
+ <!-- XRAY BAR -->
3
+
4
+ <div id="xray-bar" style="display:none">
5
+ <% if Xray.request_info.present? %>
6
+ <div id="xray-bar-controller-path">
7
+ <% if Xray.request_info[:controller] %>
8
+ <span class="xray-bar-btn xray-bar-controller xray-icon-flash" data-path="<%= Xray.request_info[:controller][:path] %>">
9
+ <b></b>
10
+ <%= Xray.request_info[:controller][:name] %><span class="xray-bar-controller-action">#<%= Xray.request_info[:controller][:action] %></span>
11
+ </span>
12
+ <% end %>
13
+ <% if Xray.request_info[:view] %>
14
+ <% layout_path = lookup_context.find(Xray.request_info[:view][:layout]).identifier %>
15
+ <span class="xray-bar-btn xray-bar-layout xray-icon-columns" data-path="<%= layout_path %>">
16
+ <b></b>
17
+ <%= layout_path.split('/').last %>
18
+ </span>
19
+ <span class="xray-bar-btn xray-bar-view xray-icon-doc" data-path="<%= Xray.request_info[:view][:path] %>">
20
+ <%= Xray.request_info[:view][:path].split('/').last %>
21
+ </span>
22
+ <% end %>
23
+ </div>
24
+ <% end %>
25
+ <div id="xray-bar-togglers">
26
+ <span class="xray-bar-btn xray-bar-all-toggler xray-icon-search"></span>
27
+ <span class="xray-bar-btn xray-bar-templates-toggler">
28
+ templates
29
+ </span>
30
+ <span class="xray-bar-btn xray-bar-views-toggler">
31
+ views
32
+ </span>
33
+ </div>
34
+ <span class="xray-bar-settings-btn xray-icon-cog"></span>
35
+ <div id="xray-settings" style="display:none">
36
+ <form>
37
+ <label for="xray-editor-input">Editor</label>
38
+ <input id="xray-editor-input" type="text" value="<%= Xray.config.editor %>">
39
+ <button type="submit">Save</button>
40
+ <p>Settings are saved to ~/.xrayconfig</p>
41
+ </form>
42
+ </div>
43
+ <%= stylesheet_link_tag :xray %>
44
+ </div>
45
+
46
+ <!-- /XRAY BAR -->
@@ -0,0 +1,52 @@
1
+ module Xray
2
+
3
+ def self.config
4
+ @@config ||= Config.new
5
+ end
6
+
7
+ class Config
8
+ attr_accessor :editor
9
+
10
+ CONFIG_FILE = "#{Dir.home}/.xrayconfig"
11
+ DEFAULT_EDITOR = '/usr/local/bin/subl'
12
+
13
+ def editor
14
+ load_config[:editor]
15
+ end
16
+
17
+ def editor=(new_editor)
18
+ if new_editor && new_editor != editor
19
+ write_config(editor: new_editor)
20
+ true
21
+ else
22
+ false
23
+ end
24
+ end
25
+
26
+ def to_yaml
27
+ {editor: editor}.to_yaml
28
+ end
29
+
30
+ private
31
+
32
+ def write_config(new_config)
33
+ config = load_config.merge(new_config)
34
+ File.open(CONFIG_FILE, 'w') { |f| f.write(config.to_yaml) }
35
+ end
36
+
37
+ def load_config
38
+ default_config.merge(local_config)
39
+ end
40
+
41
+ def local_config
42
+ YAML.load_file(CONFIG_FILE)
43
+ rescue
44
+ {}
45
+ end
46
+
47
+ def default_config
48
+ { editor: DEFAULT_EDITOR }
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,88 @@
1
+ module Xray
2
+
3
+ # This is the main point of integration with Rails. This engine hooks into
4
+ # Sprockets and monkey patches ActionView in order to augment the app's JS
5
+ # and HTML templates with filepath information that can be used by xray.js
6
+ # in the browser. It also hooks in a middleware responsible for injecting
7
+ # xray.js and the xray bar into the app's response bodies.
8
+ class Engine < ::Rails::Engine
9
+ initializer "xray.initialize" do |app|
10
+ app.middleware.use Xray::Middleware
11
+
12
+ # Register as a Sprockets processor to augment JS files, including
13
+ # compiled coffeescript, with filepath information. See
14
+ # `Xray.augment_js` for details.
15
+ app.assets.register_postprocessor 'application/javascript', :xray do |context, data|
16
+ path = context.pathname.to_s
17
+ if path =~ /^#{app.root}.+\.(js|coffee)(\.|$)/
18
+ Xray.augment_js(data, path)
19
+ else
20
+ data
21
+ end
22
+ end
23
+
24
+ # Monkey patch ActionView::Template to augment server-side templates
25
+ # with filepath information. See `Xray.augment_template` for details.
26
+ ActionView::Template.class_eval do
27
+ def render_with_xray(*args, &block)
28
+ path = identifier
29
+ source = render_without_xray(*args, &block)
30
+ if path =~ /\.(html|slim|haml)(\.|$)/ && !path.include?('_xray_bar')
31
+ Xray.augment_template(source, path)
32
+ else
33
+ source
34
+ end
35
+ end
36
+ alias_method_chain :render, :xray
37
+ end
38
+
39
+ # Augment JS templates
40
+ app.assets.register_preprocessor 'application/javascript', :xray do |context, source|
41
+ path = context.pathname.to_s
42
+ if path =~ /^#{app.root}.+\.(jst)(\.|$)/
43
+ Xray.augment_template(source, path)
44
+ else
45
+ source
46
+ end
47
+ end
48
+
49
+ # This event is called near the beginning of a request cycle. We use it to
50
+ # collect information about the controller and action that is responding, for
51
+ # display in the Xray bar.
52
+ ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |*args|
53
+ event = ActiveSupport::Notifications::Event.new(*args)
54
+ controller_name = event.payload[:controller]
55
+ action_name = event.payload[:action]
56
+ path = ActiveSupport::Dependencies.search_for_file(controller_name.underscore)
57
+
58
+ # Reset the request info hash for this request.
59
+ # NOTE: Nothing about this is thread-safe. Could this affect anyone in dev mode?
60
+ Xray.request_info.clear
61
+
62
+ Xray.request_info[:controller] = {
63
+ :path => path,
64
+ :name => controller_name,
65
+ :action => action_name
66
+ }
67
+ end
68
+
69
+ # This event is called each time during the request cycle that
70
+ # ActionView renders a template. The first time it's called will most
71
+ # likely be the view the controller is rendering, which is what we're
72
+ # interested in.
73
+ ActiveSupport::Notifications.subscribe('render_template.action_view') do |*args|
74
+ event = ActiveSupport::Notifications::Event.new(*args)
75
+ layout = event.payload[:layout]
76
+ path = event.payload[:identifier]
77
+
78
+ # We are only interested in the first notification that has a layout.
79
+ if layout
80
+ Xray.request_info[:view] ||= {
81
+ :path => path,
82
+ :layout => layout
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,88 @@
1
+ require "open3"
2
+
3
+ module Xray
4
+ OPEN_PATH = '/_xray/open'
5
+ UPDATE_CONFIG_PATH = '/_xray/config'
6
+
7
+ # This middleware is responsible for injecting xray.js, xray-backbone.js, and
8
+ # the Xray bar into the app's pages. It also listens for requests to open files
9
+ # with the user's editor.
10
+ class Middleware
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ # Request for opening a file path.
17
+ if env['PATH_INFO'] == OPEN_PATH
18
+ req, res = Rack::Request.new(env), Rack::Response.new
19
+ out, err, status = Xray.open_file(req.GET['path'])
20
+ if status.success?
21
+ res.status = 200
22
+ else
23
+ res.write out
24
+ res.status = 500
25
+ end
26
+ res.finish
27
+ elsif env['PATH_INFO'] == UPDATE_CONFIG_PATH
28
+ req, res = Rack::Request.new(env), Rack::Response.new
29
+ if req.post? && Xray.config.editor = req.POST['editor']
30
+ res.status = 200
31
+ else
32
+ res.status = 400
33
+ end
34
+ res.finish
35
+ # Inject xray.js and friends if it's a plain ol' successful HTML request.
36
+ else
37
+ status, headers, response = @app.call(env)
38
+ if should_inject_xray?(status, headers, response)
39
+ body = response.body.sub(/<body[^>]*>/) { "#{$~}\n#{xray_bar}" }
40
+ append_js!(body, 'jquery', :xray)
41
+ append_js!(body, 'backbone', :'xray-backbone')
42
+ headers['Content-Length'] = body.bytesize.to_s
43
+ end
44
+ [status, headers, (body ? [body] : response)]
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def xray_bar
51
+ ActionController::Base.new.render_to_string(:partial => '/xray_bar').html_safe
52
+ end
53
+
54
+ # Appends the given `script_name` after the `after_script_name`.
55
+ def append_js!(html, after_script_name, script_name)
56
+ # Matches:
57
+ # <script src="/assets/jquery.js"></script>
58
+ # <script src="/assets/jquery-min.js"></script>
59
+ # <script src="/assets/jquery.min.1.9.1.js"></script>
60
+ html.sub!(/<script[^>]+#{after_script_name}([-.]{1}[\d\.]+)?([-.]{1}min)?\.js[^>]+><\/script>/) do
61
+ h = ActionController::Base.helpers
62
+ "#{$~}\n" + h.javascript_include_tag(script_name)
63
+ end
64
+ end
65
+
66
+ def should_inject_xray?(status, headers, response)
67
+ status == 200 &&
68
+ html_request?(headers, response) &&
69
+ !file?(headers) &&
70
+ !empty?(response) &&
71
+ !response.body.frozen?
72
+ end
73
+
74
+ def empty?(response)
75
+ # response may be ["Not Found"], ["Move Permanently"], etc.
76
+ (response.is_a?(Array) && response.size <= 1) ||
77
+ !response.respond_to?(:body) || response.body.empty?
78
+ end
79
+
80
+ def file?(headers)
81
+ headers["Content-Transfer-Encoding"] == "binary"
82
+ end
83
+
84
+ def html_request?(headers, response)
85
+ headers['Content-Type'] && headers['Content-Type'].include?('text/html') && response.body.include?("<html")
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module Xray
2
+ VERSION = "0.1.1"
3
+ end
data/lib/xray-rails.rb ADDED
@@ -0,0 +1,106 @@
1
+ require "json"
2
+ require "active_support/all"
3
+ require_relative "xray/version"
4
+ require_relative "xray/config"
5
+ require_relative "xray/middleware"
6
+
7
+ if defined?(Rails) && Rails.env.development?
8
+ require "xray/engine"
9
+ end
10
+
11
+ module Xray
12
+ FILE_PLACEHOLDER = '$file'
13
+
14
+ # Used to collect request information during each request cycle for use in
15
+ # the Xray bar.
16
+ # TODO: there's nothing thread-safe about this. Not sure how big of a deal that is.
17
+ def self.request_info
18
+ @request_info ||= {}
19
+ end
20
+
21
+ # Patterns for the kind of JS constructors Xray is interested in knowing the
22
+ # filepath of. Unforunately, these patterns will result in a lot of false
23
+ # positives, because we can't only match direct Backbone.View subclasses -
24
+ # the app's JS may have a more complex class hierarchy than that.
25
+ CONSTRUCTOR_PATTERNS = [
26
+ '(?!jQuery|_)[\w\.]+\.extend\({', # Match uses of extend(), excluding jQuery and underscore
27
+ '\(function\(_super\) {' # Coffeescript-generated constructors
28
+ ]
29
+
30
+ # Example matches:
31
+ # MyView = Backbone.View.extend({ ...
32
+ # Foo.MyView = Backbone.View.extend({ ...
33
+ # MyView = (function(_super) { ...
34
+ #
35
+ # Captures:
36
+ # $1 = space before the constructor
37
+ # $2 = the constructor's name
38
+ # $3 = the beginning of the constructor function
39
+ CONSTRUCTOR_REGEX = /^( *)([\w\.]+) *= *(#{CONSTRUCTOR_PATTERNS.join('|')})/
40
+
41
+ # Returns augmented JS source where constructors Xray wants to know the
42
+ # filepath of are captured in such a way that at runtime, xray.js can look
43
+ # up a view constructor's filepath and name.
44
+ #
45
+ # This:
46
+ # MyView = Backbone.View.extend({ ...
47
+ #
48
+ # Becomes:
49
+ # MyView = (window.XrayPaths||(window.XrayPaths={}))['{"name":"MyView","path":"/path/to/file.js"}'] = Backbone.View.extend({ ...
50
+ #
51
+ # A goal here was to not add any new lines to the source so as not to throw
52
+ # off line numbers if an exception is thrown, hence the odd pattern of
53
+ # abusing an object set operation in a multiple assignment.
54
+ #
55
+ # TODO: This is simple and gets the job done, but is a bit ridiculous.
56
+ # I've also seen this appear in stack traces :( Would love to find a
57
+ # way to do this without actually writing to the files.
58
+ def self.augment_js(source, path)
59
+ source.gsub(CONSTRUCTOR_REGEX) do
60
+ space, class_name, func = $1, $2, $3
61
+ info = {name: class_name, path: path.to_s}
62
+ xray = "(window.XrayPaths||(window.XrayPaths={}))['#{info.to_json}']"
63
+ "#{space}#{class_name} = #{xray} = #{func}"
64
+ end
65
+ end
66
+
67
+ # Returns augmented HTML where the source is simply wrapped in an HTML
68
+ # comment with filepath info. Xray.js uses these comments to associate
69
+ # elements with the tempaltes that rendered them.
70
+ #
71
+ # This:
72
+ # <div class=".my-element">
73
+ # ...
74
+ # </div>
75
+ #
76
+ # Becomes:
77
+ # <!-- XRAY START 123 /path/to/file.html -->
78
+ # <div class=".my-element">
79
+ # ...
80
+ # </div>
81
+ # <!-- XRAY END 123 -->
82
+ def self.augment_template(source, path)
83
+ id = next_id
84
+ # skim doesn't allow html comments, so use skim's comment syntax if it's skim
85
+ if path =~ /\.(skim)(\.|$)/
86
+ augmented = "/!XRAY START #{id} #{path}\n#{source}\n/!XRAY END #{id}"
87
+ else
88
+ augmented = "<!--XRAY START #{id} #{path}-->\n#{source}\n<!--XRAY END #{id}-->"
89
+ end
90
+ ActiveSupport::SafeBuffer === source ? ActiveSupport::SafeBuffer.new(augmented) : augmented
91
+ end
92
+
93
+ def self.next_id
94
+ @id = (@id ||= 0) + 1
95
+ end
96
+
97
+ def self.open_file(file)
98
+ editor = Xray.config.editor
99
+ cmd = if editor.include?('$file')
100
+ editor.gsub '$file', file
101
+ else
102
+ "#{editor} #{file}"
103
+ end
104
+ Open3.capture3(cmd)
105
+ end
106
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xray-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brent Dillingham
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: coffee-rails
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec-rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sqlite3
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: jquery-rails
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: backbone-rails
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sass-rails
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: eco
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: capybara
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - '='
148
+ - !ruby/object:Gem::Version
149
+ version: 2.1.0
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - '='
156
+ - !ruby/object:Gem::Version
157
+ version: 2.1.0
158
+ description: Provides a dev bar and an overlay in-browser to visualize your UI's rendered
159
+ partials and Backbone views
160
+ email:
161
+ - brentdillingham@gmail.com
162
+ executables: []
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - app/assets/javascripts/xray-backbone.js.coffee
167
+ - app/assets/javascripts/xray.js.coffee
168
+ - app/assets/stylesheets/xray.css
169
+ - app/views/_xray_bar.html.erb
170
+ - lib/xray/config.rb
171
+ - lib/xray/engine.rb
172
+ - lib/xray/middleware.rb
173
+ - lib/xray/version.rb
174
+ - lib/xray-rails.rb
175
+ - LICENSE
176
+ - README.md
177
+ homepage: https://github.com/brentd/xray-rails
178
+ licenses: []
179
+ post_install_message:
180
+ rdoc_options: []
181
+ require_paths:
182
+ - lib
183
+ required_ruby_version: !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ! '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ none: false
191
+ requirements:
192
+ - - ! '>='
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 1.8.23
198
+ signing_key:
199
+ specification_version: 3
200
+ summary: Reveal the structure of your UI
201
+ test_files: []