twine-rails 0.0.5
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/LICENSE +21 -0
- data/README.md +111 -0
- data/lib/assets/javascripts/twine.js.coffee +293 -0
- data/lib/twine-rails/version.rb +3 -0
- data/lib/twine-rails.rb +9 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5e3dd60b497b3a0ceea4b930d21396ce2b589421
|
4
|
+
data.tar.gz: 98f97ae534740b9acf588183825169cec31fe172
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7e440905d00db7bdd0505d717ddd831579887ea4070dfce795fb77de64cec83a621875153a3c7ece28e4ff67aae011e049548d788dd3def4a63aa49da9ef569c
|
7
|
+
data.tar.gz: 6299d2b9385b20b9086b6f4d100d02bc030270e78f3d6c8eb7fc90265ee6432cae7e6ec66f45469867a011a3bd724658a5a0b6a0996bd1cc0fe30130563a2e6b
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Shopify Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
twine
|
2
|
+
-----
|
3
|
+
|
4
|
+
[](http://travis-ci.org/Shopify/twine)
|
5
|
+
|
6
|
+
[Demonstration](http://shopify.github.io/twine/)
|
7
|
+
|
8
|
+
Twine is a minimalistic two-way binding system.
|
9
|
+
|
10
|
+
Features:
|
11
|
+
- It's just JS - no new syntax to learn for bindings
|
12
|
+
- Small and easy to understand codebase
|
13
|
+
|
14
|
+
Non-features:
|
15
|
+
- No creation of new nodes (e.g. no iteration)
|
16
|
+
- No special declaration of bindable data (i.e. bind to any JS data)
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Twine is available on bower via `bower install twine` if that is your preference.
|
21
|
+
|
22
|
+
Twine comes as `dist/twine.js` and `dist/twine.min.js` in this repo and in the bower package.
|
23
|
+
|
24
|
+
Twine is also available as a gem. In your Gemfile, add `gem 'twine-rails'` and include it in your `application.js` manifest via `//= require twine`
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Twine can be initialized simply with the following:
|
29
|
+
|
30
|
+
```html
|
31
|
+
<script type="text/javascript">
|
32
|
+
context = {}
|
33
|
+
$(function() {
|
34
|
+
Twine.reset(context).bind().refresh()
|
35
|
+
});
|
36
|
+
</script>
|
37
|
+
```
|
38
|
+
|
39
|
+
Above, `context` will be considered the context root, and this will work until you navigate to a new page. On a simple app, this may be all you need to do.
|
40
|
+
|
41
|
+
### With [rails/turbolinks](https://github.com/rails/turbolinks)
|
42
|
+
|
43
|
+
Turbolinks requires a bit more consideration, as the executing JS context will remain the same -- you have the same `window` object throughout operation. When the page changes and new nodes come in, they need to be re-bound manually. Twine leaves this to you, rather than attempting to guess.
|
44
|
+
|
45
|
+
Here's a sample snippet that you might use:
|
46
|
+
|
47
|
+
```coffee
|
48
|
+
context = {}
|
49
|
+
|
50
|
+
document.addEventListener 'page:change', ->
|
51
|
+
Twine.reset(context).bind().refresh()
|
52
|
+
return
|
53
|
+
```
|
54
|
+
|
55
|
+
If you're using the [jquery.turbolinks gem](https://github.com/kossnocorp/jquery.turbolinks), then you can use:
|
56
|
+
|
57
|
+
```coffee
|
58
|
+
context = {}
|
59
|
+
|
60
|
+
$ ->
|
61
|
+
Twine.reset(context).bind().refresh()
|
62
|
+
return
|
63
|
+
```
|
64
|
+
|
65
|
+
### With [Shopify/turbograft](https://github.com/Shopify/turbograft)
|
66
|
+
|
67
|
+
With TurboGraft, you may have cases where you want to keep parts of the page around, and thus, their bindings should continue to live.
|
68
|
+
|
69
|
+
The following snippet may help:
|
70
|
+
|
71
|
+
```coffee
|
72
|
+
context = {}
|
73
|
+
|
74
|
+
reset = (nodes) ->
|
75
|
+
if nodes
|
76
|
+
Twine.bind(node) for node in nodes
|
77
|
+
else
|
78
|
+
Twine.reset(context).bind()
|
79
|
+
|
80
|
+
Twine.refreshImmediately()
|
81
|
+
return
|
82
|
+
|
83
|
+
document.addEventListener 'DOMContentLoaded', -> reset()
|
84
|
+
|
85
|
+
document.addEventListener 'page:load', (event) ->
|
86
|
+
reset(event.data)
|
87
|
+
return
|
88
|
+
|
89
|
+
document.addEventListener 'page:before-partial-replace', (event) ->
|
90
|
+
nodes = event.data
|
91
|
+
Twine.unbind(node) for node in nodes
|
92
|
+
return
|
93
|
+
|
94
|
+
$(document).ajaxComplete ->
|
95
|
+
Twine.refresh()
|
96
|
+
```
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
1. Clone the repo: `git clone git@github.com:Shopify/twine`
|
101
|
+
2. `cd twine`
|
102
|
+
3. `npm install`
|
103
|
+
4. `npm install -g testem coffee-script`
|
104
|
+
5. Run the tests using `testem`, or `testem ci`
|
105
|
+
6. Submit a PR
|
106
|
+
|
107
|
+
## Releasing
|
108
|
+
|
109
|
+
1. Update version number in `package.json`, `bower.json`, and `lib/twine-rails/version.rb`
|
110
|
+
2. Run `bundle install` to update `Gemfile.lock`
|
111
|
+
3. Push the new tag to GitHub and the new version to rubygems with `bundle exec rake release`
|
@@ -0,0 +1,293 @@
|
|
1
|
+
window.Twine = {}
|
2
|
+
Twine.shouldDiscardEvent = {}
|
3
|
+
|
4
|
+
# Map of node binding ids to objects that describe a node's bindings.
|
5
|
+
elements = {}
|
6
|
+
|
7
|
+
# The number of nodes bound since the last call to Twine.reset().
|
8
|
+
# Used to determine the next binding id.
|
9
|
+
nodeCount = 0
|
10
|
+
|
11
|
+
# Storage for all bindable data, provided by the caller of Twine.reset().
|
12
|
+
rootContext = null
|
13
|
+
|
14
|
+
keypathRegex = /^[a-z]\w*(\.[a-z]\w*|\[\d+\])*$/i # Tests if a string is a pure keypath.
|
15
|
+
refreshQueued = false
|
16
|
+
rootNode = null
|
17
|
+
|
18
|
+
# Cleans up all existing bindings and sets the root node and context.
|
19
|
+
Twine.reset = (newContext, node = document.documentElement) ->
|
20
|
+
for bindingsArr in elements
|
21
|
+
obj.teardown() for obj in bindingsArr when obj.teardown
|
22
|
+
elements = {}
|
23
|
+
|
24
|
+
rootContext = newContext
|
25
|
+
rootNode = node
|
26
|
+
rootNode.bindingId = nodeCount = 1
|
27
|
+
|
28
|
+
this
|
29
|
+
|
30
|
+
Twine.bind = (node = rootNode, context = Twine.context(node)) ->
|
31
|
+
bind(context, node, true)
|
32
|
+
|
33
|
+
bind = (context, node, forceSaveContext) ->
|
34
|
+
if node.bindingId
|
35
|
+
Twine.unbind(node)
|
36
|
+
|
37
|
+
for type, binding of Twine.bindingTypes when definition = node.getAttribute(type)
|
38
|
+
element = {bindings: []} unless element # Defer allocation to prevent GC pressure
|
39
|
+
fn = binding(node, context, definition, element)
|
40
|
+
element.bindings.push(fn) if fn
|
41
|
+
|
42
|
+
if newContextKey = node.getAttribute('context')
|
43
|
+
keypath = keypathForKey(newContextKey)
|
44
|
+
if keypath[0] == '$root'
|
45
|
+
context = rootContext
|
46
|
+
keypath = keypath.slice(1)
|
47
|
+
context = getValue(context, keypath) || setValue(context, keypath, {})
|
48
|
+
|
49
|
+
if element || newContextKey || forceSaveContext
|
50
|
+
(element ?= {}).childContext = context
|
51
|
+
elements[node.bindingId ?= ++nodeCount] = element
|
52
|
+
|
53
|
+
# IE and Safari don't support node.children for DocumentFragment and SVGElement nodes.
|
54
|
+
# If the element supports children we continue to traverse the children, otherwise
|
55
|
+
# we stop traversing that subtree.
|
56
|
+
# https://developer.mozilla.org/en-US/docs/Web/API/ParentNode.children
|
57
|
+
# As a result, Twine are unsupported within DocumentFragment and SVGElement nodes.
|
58
|
+
bind(context, childNode) for childNode in (node.children || [])
|
59
|
+
Twine.count = nodeCount
|
60
|
+
Twine
|
61
|
+
|
62
|
+
# Queues a refresh of the DOM, batching up calls for the current synchronous block.
|
63
|
+
Twine.refresh = ->
|
64
|
+
return if refreshQueued
|
65
|
+
refreshQueued = true
|
66
|
+
setTimeout(Twine.refreshImmediately, 0)
|
67
|
+
|
68
|
+
refreshElement = (element) ->
|
69
|
+
(obj.refresh() if obj.refresh?) for obj in element.bindings if element.bindings
|
70
|
+
return
|
71
|
+
|
72
|
+
Twine.refreshImmediately = ->
|
73
|
+
refreshQueued = false
|
74
|
+
refreshElement(element) for key, element of elements
|
75
|
+
return
|
76
|
+
|
77
|
+
# Force the binding system to recognize programmatic changes to a node's value.
|
78
|
+
Twine.change = (node, bubble = false) ->
|
79
|
+
event = document.createEvent("HTMLEvents")
|
80
|
+
event.initEvent('change', bubble, true) # for IE 9/10 compatibility.
|
81
|
+
node.dispatchEvent(event)
|
82
|
+
|
83
|
+
# Cleans up everything related to a node and its subtree.
|
84
|
+
Twine.unbind = (node) ->
|
85
|
+
if id = node.bindingId
|
86
|
+
if bindings = elements[id]?.bindings
|
87
|
+
obj.teardown() for obj in bindings when obj.teardown
|
88
|
+
delete elements[id]
|
89
|
+
# IE and Safari don't support node.children for DocumentFragment or SVGElement,
|
90
|
+
# See explaination in bind()
|
91
|
+
Twine.unbind(childNode) for childNode in (node.children || [])
|
92
|
+
this
|
93
|
+
|
94
|
+
# Returns the binding context for a node by looking up the tree.
|
95
|
+
Twine.context = (node) -> getContext(node, false)
|
96
|
+
Twine.childContext = (node) -> getContext(node, true)
|
97
|
+
|
98
|
+
getContext = (node, child) ->
|
99
|
+
while node
|
100
|
+
return rootContext if node == rootNode
|
101
|
+
node = node.parentNode if !child
|
102
|
+
if (id = node.bindingId) && (context = elements[id]?.childContext)
|
103
|
+
return context
|
104
|
+
node = node.parentNode if child
|
105
|
+
|
106
|
+
# Returns the fully qualified key for a node's context
|
107
|
+
Twine.contextKey = (node, lastContext) ->
|
108
|
+
keys = []
|
109
|
+
addKey = (context) ->
|
110
|
+
for key, val of context when lastContext == val
|
111
|
+
keys.unshift(key)
|
112
|
+
break
|
113
|
+
lastContext = context
|
114
|
+
|
115
|
+
while node && node != rootNode && node = node.parentNode
|
116
|
+
addKey(context) if (id = node.bindingId) && (context = elements[id]?.childContext)
|
117
|
+
|
118
|
+
addKey(rootContext) if node == rootNode
|
119
|
+
keys.join('.')
|
120
|
+
|
121
|
+
valueAttributeForNode = (node) ->
|
122
|
+
name = node.nodeName.toLowerCase()
|
123
|
+
if name in ['input', 'textarea', 'select']
|
124
|
+
if node.getAttribute('type') in ['checkbox', 'radio'] then 'checked' else 'value'
|
125
|
+
else
|
126
|
+
'textContent'
|
127
|
+
|
128
|
+
keypathForKey = (key) ->
|
129
|
+
keypath = []
|
130
|
+
for key in key.split('.')
|
131
|
+
if (start = key.indexOf('[')) != -1
|
132
|
+
keypath.push(key.substr(0, start))
|
133
|
+
key = key.substr(start)
|
134
|
+
|
135
|
+
while (end = key.indexOf(']')) != -1
|
136
|
+
keypath.push(parseInt(key.substr(1, end), 10))
|
137
|
+
key = key.substr(end + 1)
|
138
|
+
else
|
139
|
+
keypath.push(key)
|
140
|
+
keypath
|
141
|
+
|
142
|
+
getValue = (object, keypath) ->
|
143
|
+
object = object[key] for key in keypath when object?
|
144
|
+
object
|
145
|
+
|
146
|
+
setValue = (object, keypath, value) ->
|
147
|
+
[keypath..., lastKey] = keypath
|
148
|
+
for key in keypath
|
149
|
+
object = object[key] ?= {}
|
150
|
+
object[lastKey] = value
|
151
|
+
|
152
|
+
stringifyNodeAttributes = (node) ->
|
153
|
+
nAttributes = node.attributes.length
|
154
|
+
i = 0
|
155
|
+
result = ""
|
156
|
+
while i < nAttributes
|
157
|
+
attr = node.attributes.item(i)
|
158
|
+
result += "#{attr.nodeName}='#{attr.textContent}'"
|
159
|
+
i+=1
|
160
|
+
result
|
161
|
+
|
162
|
+
wrapFunctionString = (code, args, node) ->
|
163
|
+
if isKeypath(code) && keypath = keypathForKey(code)
|
164
|
+
if keypath[0] == '$root'
|
165
|
+
($context, $root) -> getValue($root, keypath)
|
166
|
+
else
|
167
|
+
($context, $root) -> getValue($context, keypath)
|
168
|
+
else
|
169
|
+
try
|
170
|
+
new Function(args, "with($context) { return #{code} }")
|
171
|
+
catch e
|
172
|
+
throw "Twine error: Unable to create function on #{node.nodeName} node with attributes #{stringifyNodeAttributes(node)}"
|
173
|
+
|
174
|
+
isKeypath = (value) ->
|
175
|
+
value not in ['true', 'false', 'null', 'undefined'] and keypathRegex.test(value)
|
176
|
+
|
177
|
+
fireCustomChangeEvent = (node) ->
|
178
|
+
event = document.createEvent('CustomEvent')
|
179
|
+
event.initCustomEvent('bindings:change', true, false, {})
|
180
|
+
node.dispatchEvent(event)
|
181
|
+
|
182
|
+
Twine.bindingTypes =
|
183
|
+
bind: (node, context, definition) ->
|
184
|
+
valueAttribute = valueAttributeForNode(node)
|
185
|
+
value = node[valueAttribute]
|
186
|
+
lastValue = undefined
|
187
|
+
teardown = undefined
|
188
|
+
|
189
|
+
# Radio buttons only set the value to the node value if checked.
|
190
|
+
checkedValueType = node.getAttribute('type') == 'radio'
|
191
|
+
fn = wrapFunctionString(definition, '$context,$root', node)
|
192
|
+
|
193
|
+
refresh = ->
|
194
|
+
newValue = fn.call(node, context, rootContext)
|
195
|
+
return if newValue == lastValue
|
196
|
+
|
197
|
+
lastValue = newValue
|
198
|
+
return if newValue == node[valueAttribute]
|
199
|
+
|
200
|
+
node[valueAttribute] = if checkedValueType then newValue == node.value else newValue
|
201
|
+
fireCustomChangeEvent(node)
|
202
|
+
|
203
|
+
return {refresh} unless isKeypath(definition)
|
204
|
+
|
205
|
+
refreshContext = ->
|
206
|
+
if checkedValueType
|
207
|
+
return unless node.checked
|
208
|
+
setValue(context, keypath, node.value)
|
209
|
+
else
|
210
|
+
setValue(context, keypath, node[valueAttribute])
|
211
|
+
|
212
|
+
keypath = keypathForKey(definition)
|
213
|
+
twoWayBinding = valueAttribute != 'textContent' && node.type != 'hidden'
|
214
|
+
|
215
|
+
if keypath[0] == '$root'
|
216
|
+
context = rootContext
|
217
|
+
keypath = keypath.slice(1)
|
218
|
+
|
219
|
+
if value? && (twoWayBinding || value != '') && !(oldValue = getValue(context, keypath))?
|
220
|
+
refreshContext()
|
221
|
+
|
222
|
+
if twoWayBinding
|
223
|
+
changeHandler = ->
|
224
|
+
return if getValue(context, keypath) == this[valueAttribute]
|
225
|
+
refreshContext()
|
226
|
+
Twine.refreshImmediately()
|
227
|
+
node.addEventListener(eventKey, changeHandler) for eventKey in ['input', 'keyup', 'change']
|
228
|
+
teardown = ->
|
229
|
+
node.removeEventListener(eventKey, changeHandler) for eventKey in ['input', 'keyup', 'change']
|
230
|
+
|
231
|
+
{refresh, teardown}
|
232
|
+
|
233
|
+
'bind-show': (node, context, definition) ->
|
234
|
+
fn = wrapFunctionString(definition, '$context,$root', node)
|
235
|
+
lastValue = undefined
|
236
|
+
return refresh: ->
|
237
|
+
newValue = !fn.call(node, context, rootContext)
|
238
|
+
return if newValue == lastValue
|
239
|
+
$(node).toggleClass('hide', lastValue = newValue)
|
240
|
+
|
241
|
+
'bind-class': (node, context, definition) ->
|
242
|
+
fn = wrapFunctionString(definition, '$context,$root', node)
|
243
|
+
lastValue = {}
|
244
|
+
return refresh: ->
|
245
|
+
newValue = fn.call(node, context, rootContext)
|
246
|
+
for key, value of newValue when !lastValue[key] != !value
|
247
|
+
$(node).toggleClass(key, !!value)
|
248
|
+
lastValue = newValue
|
249
|
+
|
250
|
+
define: (node, context, definition) ->
|
251
|
+
fn = wrapFunctionString(definition, '$context,$root', node)
|
252
|
+
object = fn.call(node, context, rootContext)
|
253
|
+
context[key] = value for key, value of object
|
254
|
+
return
|
255
|
+
|
256
|
+
setupAttributeBinding = (attributeName, bindingName) ->
|
257
|
+
booleanAttribute = attributeName in ['checked', 'disabled', 'readOnly']
|
258
|
+
|
259
|
+
Twine.bindingTypes["bind-#{bindingName}"] = (node, context, definition) ->
|
260
|
+
fn = wrapFunctionString(definition, '$context,$root', node)
|
261
|
+
lastValue = undefined
|
262
|
+
return refresh: ->
|
263
|
+
newValue = fn.call(node, context, rootContext)
|
264
|
+
newValue = !!newValue if booleanAttribute
|
265
|
+
return if newValue == lastValue
|
266
|
+
node[attributeName] = lastValue = newValue
|
267
|
+
|
268
|
+
for attribute in ['placeholder', 'checked', 'disabled', 'href', 'title', 'readOnly']
|
269
|
+
setupAttributeBinding(attribute, attribute)
|
270
|
+
|
271
|
+
setupAttributeBinding('innerHTML', 'unsafe-html')
|
272
|
+
|
273
|
+
preventDefaultForEvent = (event) ->
|
274
|
+
(event.type == 'submit' || event.currentTarget.nodeName.toLowerCase() == 'a') && event.currentTarget.getAttribute('allow-default') != '1'
|
275
|
+
|
276
|
+
setupEventBinding = (eventName) ->
|
277
|
+
Twine.bindingTypes["bind-event-#{eventName}"] = (node, context, definition) ->
|
278
|
+
onEventHandler = (event, data) ->
|
279
|
+
discardEvent = Twine.shouldDiscardEvent[eventName]?(event)
|
280
|
+
if discardEvent || preventDefaultForEvent(event)
|
281
|
+
event.preventDefault()
|
282
|
+
|
283
|
+
return if discardEvent
|
284
|
+
|
285
|
+
wrapFunctionString(definition, '$context,$root,event,data', node).call(node, context, rootContext, event, data)
|
286
|
+
Twine.refreshImmediately()
|
287
|
+
$(node).on eventName, onEventHandler
|
288
|
+
|
289
|
+
return teardown: ->
|
290
|
+
$(node).off eventName, onEventHandler
|
291
|
+
|
292
|
+
for eventName in ['click', 'dblclick', 'mousedown', 'mouseup', 'submit', 'dragenter', 'dragleave', 'dragover', 'drop', 'drag', 'change', 'keypress', 'keydown', 'keyup', 'input', 'error', 'done', 'fail', 'blur', 'focus']
|
293
|
+
setupEventBinding(eventName)
|
data/lib/twine-rails.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twine-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Li
|
8
|
+
- Kristian Plettenberg-Dussault
|
9
|
+
- Anthony Cameron
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2014-10-05 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: coffee-rails
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: bundler
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rake
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
description:
|
58
|
+
email:
|
59
|
+
- jli@shopify.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- lib/assets/javascripts/twine.js.coffee
|
67
|
+
- lib/twine-rails.rb
|
68
|
+
- lib/twine-rails/version.rb
|
69
|
+
homepage: https://github.com/Shopify/twine
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata: {}
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 2.2.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: Minimalistic two-way bindings
|
93
|
+
test_files: []
|