tubby 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.md +12 -0
  4. data/README.md +393 -0
  5. data/lib/tubby.rb +137 -0
  6. metadata +48 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 23fa67b928c22ddfe8a88d2a31d9aec059babbb7
4
+ data.tar.gz: c04266360faf36cc54e517b264f978c61a47867b
5
+ SHA512:
6
+ metadata.gz: a747e3bb0a99977634b781d7892c129194e931a3b3de8d133088d74a9e542be7f5fc4308c7151e90d8ab7774d709eae34e129783540fc97366debcbad3547b5f
7
+ data.tar.gz: 61e61741fee0eb09789521f77098c64ef4f4e12172da9a5aa48088fa09e120346e4849dba23778c9fd54eb157455d2168be4fbf89ea07a194f1cca546ed3d39c
@@ -0,0 +1,6 @@
1
+ # Tubby CHANGELOG
2
+
3
+ ## Changes since previous release
4
+
5
+ - Initial implementation
6
+
@@ -0,0 +1,12 @@
1
+ Copyright (C) 2018 Magnus Holm <judofyr@gmail.com>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
8
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
10
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
11
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
12
+ PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,393 @@
1
+ # Tubby: Tags in Ruby
2
+
3
+ Tubby is a lightweight library for writing HTML components in plain Ruby.
4
+
5
+ ```ruby
6
+ tmpl = Tubby.new { |t|
7
+ t.doctype!
8
+
9
+ t.h1("Hello #{current_user}!")
10
+
11
+ t << Avatar.new(current_user)
12
+
13
+ t.ul {
14
+ t.li("Tinky Winky")
15
+ t.li("Dipsy", class: "active")
16
+ t.li("Laa-Laa")
17
+ t.li("Po")
18
+ }
19
+ }
20
+
21
+ class Avatar
22
+ def initialize(user)
23
+ @user = user
24
+ end
25
+
26
+ def url
27
+ # Calculate URL
28
+ end
29
+
30
+ def to_tubby
31
+ Tubby.new { |t|
32
+ t.div(class: "avatar") {
33
+ t.img(src: url)
34
+ }
35
+ }
36
+ end
37
+ end
38
+
39
+ puts tmpl.to_s
40
+ ```
41
+
42
+ Table of contents:
43
+
44
+ - [Basic usage](#basic-usage)
45
+ - [Advanced usage](#advanced-usage)
46
+ - [Versioning](#versioning)
47
+ - [License](#license)
48
+
49
+ ## Basic usage
50
+
51
+ ### Creating templates
52
+
53
+ `Tubby.new` accepts a block and returns a `Tubby::Template`:
54
+
55
+ ```ruby
56
+ tmpl = Tubby.new { |t|
57
+ # content inside here
58
+ }
59
+ ```
60
+
61
+ The block will be executed once you call `#to_s` (or its alias `#to_html`):
62
+
63
+ ```ruby
64
+ puts tmpl.to_s
65
+ ```
66
+
67
+ ### Writing HTML tags
68
+
69
+ The following forms are available for writing HTML inside a template:
70
+
71
+ ```ruby
72
+ # Empty tag
73
+ t.h1
74
+ # => <h1></h1>
75
+
76
+ # Tag with content
77
+ t.h1("Welcome!")
78
+ # => <h1>Welcome!</h1>
79
+
80
+ # Tag with attributes
81
+ t.h1(class: "big")
82
+ # => <h1 class="big"></h1>
83
+
84
+ # Tag with attributes and content
85
+ t.h1("Welcome!", class: "big")
86
+ # => <h1 class="big">Welcome!</h1>
87
+
88
+ # Tag with block content
89
+ t.h1 {
90
+ t.span("Hello")
91
+ }
92
+ # => <h1><span>Hello</span></h1>
93
+
94
+ # Tag with block content and attributes
95
+ t.h1(class: "big") {
96
+ t.span("Hello")
97
+ }
98
+ # => <h1 class="big"><span>Hello</span></h1>
99
+
100
+ # Tag with block content, attributes and content
101
+ t.h1("Hello ", class: "big") {
102
+ t.span("world!")
103
+ }
104
+ # => <h1 class="big">Hello <span>world!</span></h1>
105
+ ```
106
+
107
+ It's recommended to use `{ }`for nesting of tags and `do/end` for nesting of
108
+ control flow. At first it looks weird, but otherwise it becomes hard to
109
+ visualize the control flow:
110
+
111
+ ```ruby
112
+ t.ul {
113
+ users.each do |user|
114
+ t.li {
115
+ t.a(user.name, href: user_path(user))
116
+ }
117
+ end
118
+ }
119
+ ```
120
+
121
+ ### Writing attributes
122
+
123
+ Tubby supports various ways of writing attributes:
124
+
125
+ ```ruby
126
+ # Plain attribute
127
+ t.input(value: "hello")
128
+ # => <input value="hello">
129
+
130
+ # nil/false values ignores the attribute
131
+ t.input(value: nil)
132
+ # => <input>
133
+
134
+ # A true value doesn't generate a value
135
+ t.input(checked: true)
136
+ # => <input checked>
137
+
138
+ # An array will be space-joined
139
+ t.input(class: ["form-control", "error"])
140
+ # => <input class="form-control error">
141
+
142
+ # ... but nil values are ignored
143
+ t.input(class: ["form-control", ("error" if error)])
144
+ # => <input class="form-control">
145
+ # => <input class="form-control error">
146
+ ```
147
+
148
+ ### Writing plain text
149
+
150
+ Inside a template you can use `<<` to append text:
151
+
152
+ ```ruby
153
+ t.h1 {
154
+ t << "Hello "
155
+ t.strong("world")
156
+ t << "!"
157
+ }
158
+ # => <h1>Hello <strong>world</strong>!</h1>
159
+ ```
160
+
161
+ By default, `#to_s` will be called and the value will be escaped:
162
+
163
+ ```ruby
164
+ t.h1 {
165
+ t << "Hello & world"
166
+ }
167
+ # => <h1>Hello &amp; world</h1>
168
+ ```
169
+
170
+ There are three ways to avoid escaping:
171
+
172
+ ```ruby
173
+ class Other
174
+ def to_html
175
+ "<custom>"
176
+ end
177
+ end
178
+
179
+ # (1) Appending an object which implements #to_html. Tubby will call the method
180
+ # and append the result without escaping it
181
+ t << Other.new
182
+
183
+ # (2) If you're using Rails, html_safe? is respected
184
+ t << "<custom>".html_safe!
185
+
186
+ # (3) There's also a separate helper
187
+ t.raw!("<custom>")
188
+ ```
189
+
190
+ In addition, there's a helper for writing a HTML5 doctype:
191
+
192
+ ```ruby
193
+ t.doctype!
194
+ # => <!DOCTYPE html>
195
+ ```
196
+
197
+ ### Appending other templates
198
+
199
+ You can also append another template:
200
+
201
+ ```ruby
202
+ content = Tubby.new { |t|
203
+ t.h1("Users")
204
+ }
205
+
206
+ main = Tubby.new { |t|
207
+ t.doctype!
208
+ t.head {
209
+ t.title("My App")
210
+ }
211
+
212
+ t.body {
213
+ t << content
214
+ }
215
+ }
216
+ ```
217
+
218
+ This is the main building block for creating composable templates.
219
+
220
+ ### Implementing `#to_tubby`
221
+
222
+ Before appending, Tubby will call the `#to_tubby` method if it exists:
223
+
224
+ ```ruby
225
+ class Avatar
226
+ def initialize(user)
227
+ @user = user
228
+ end
229
+
230
+ def url
231
+ # Calculate URL
232
+ end
233
+
234
+ def to_tubby
235
+ Tubby.new { |t|
236
+ t.div(class: "avatar") {
237
+ t.img(src: url)
238
+ }
239
+ }
240
+ end
241
+ end
242
+
243
+ tmpl = Tubby.new { |t|
244
+ t << Avatar.new(user)
245
+ }
246
+ ```
247
+
248
+ `#to_tubby` can return any value that `<<` accepts (i.e. strings that will be
249
+ escaped, objects that respond to `#to_html` and so on), but most of the time you
250
+ want to create a new template object.
251
+
252
+ ## Advanced usage
253
+
254
+ The variable `t` in all of the examples above is an instance of
255
+ `Tubby::Renderer`. Calling `Tubby::Template#to_s` is a shortcut for the
256
+ following:
257
+
258
+ ```ruby
259
+ tmpl = Tubby.new { |t|
260
+ # content inside here
261
+ }
262
+
263
+ # This:
264
+ puts tmpl.to_s
265
+
266
+ # ... is the same as:
267
+ target = String.new
268
+ t = Tubby::Renderer.new(target)
269
+ t << tmpl
270
+ puts target
271
+ ```
272
+
273
+ Let's look at two ways we can customize Tubby.
274
+
275
+ ### Custom target
276
+
277
+ The target object doesn't have to be a String, it must only be an object which
278
+ responds to `<<`. Using a custom target might be useful if you want stream the
279
+ HTML directly into a socket/file. For instance, this will print the HTML out to
280
+ the standard output:
281
+
282
+ ```ruby
283
+ tmpl = Tubby.new { |t|
284
+ t.h1("Hello terminal!")
285
+ }
286
+
287
+ t = Tubby::Renderer.new($stdout)
288
+ t << tmpl
289
+ ```
290
+
291
+ ### Custom renderer
292
+
293
+ You are also free to subclass the Renderer to provide additional helpers/data:
294
+
295
+ ```ruby
296
+ tmpl = Tubby.new { |t|
297
+ t.post_form(action: t.login_path) {
298
+ t.input(name: "username")
299
+ t.input(type: "password", name: "password")
300
+ }
301
+ }
302
+
303
+ class Renderer < Tubby::Renderer
304
+ include URLHelpers
305
+
306
+ attr_accessor :csrf_token
307
+
308
+ # Renders a <form>-tag with the csrf_token
309
+ def post_form(**opts)
310
+ form(method: "post", **opts) {
311
+ input(type: "hidden", name: "csrf_token", value: csrf_token)
312
+ yield
313
+ }
314
+ end
315
+ end
316
+
317
+ target = String.new
318
+ t = Renderer.new(target)
319
+ t.csrf_token = "hello"
320
+ t << tmpl
321
+ puts target
322
+ ```
323
+
324
+ You should use this feature with care as it makes your components coupled to the
325
+ data you provide. For instance, it might be tempting to have access to the Rack
326
+ environment as `t.rack_env`, but this means you can no longer render any HTML
327
+ outside of a Rack context (e.g: generating email). For CSRF token it makes
328
+ sense: it's a value which is global for the whole page, you might need it deeply
329
+ nested inside a component, and it's a hassle to pass it along.
330
+
331
+ In general however you should prefer separate classes over custom renderer methods:
332
+
333
+ ```ruby
334
+ # Do this:
335
+
336
+ class OkCancel
337
+ def initialize(cancel_link:)
338
+ @cancel_link = cancel_link
339
+ end
340
+
341
+ def to_tubby
342
+ Tubby.new { |t|
343
+ t.div(class: "btn-group") {
344
+ t.button("Save", class: "btn", type: "submit")
345
+ t.a("Cancel", class: "btn", href: @cancel_link)
346
+ }
347
+ }
348
+ end
349
+ end
350
+
351
+ tmpl = Tubby.new { |t|
352
+ t << OkCancel.new(cancel_link: "/users")
353
+ }
354
+
355
+ # Don't do this:
356
+
357
+ class Renderer < Tubby::Renderer
358
+ def ok_cancel(cancel_link:)
359
+ Tubby.new { |t|
360
+ t.div(class: "btn-group") {
361
+ t.button("Save", class: "btn", type: "submit")
362
+ t.a("Cancel", class: "btn", href: cancel_link)
363
+ }
364
+ }
365
+ end
366
+ end
367
+
368
+ tmpl = Tubby.new { |t|
369
+ t.ok_cancel(cancel_link: "/users")
370
+ }
371
+ ```
372
+
373
+ ## Versioning
374
+
375
+ Tubby uses version numbers on the form MAJOR.MINOR, and releases are backwards
376
+ compatible with earlier releases with the same MAJOR version.
377
+
378
+ ## License
379
+
380
+ Tubby is is available under the 0BSD license:
381
+
382
+ > Copyright (C) 2018 Magnus Holm <judofyr@gmail.com>
383
+ >
384
+ > Permission to use, copy, modify, and/or distribute this software for any
385
+ > purpose with or without fee is hereby granted.
386
+ >
387
+ > THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
388
+ > REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
389
+ > AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
390
+ > INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
391
+ > LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
392
+ > OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
393
+ > PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+ require "cgi"
3
+
4
+ module Tubby
5
+ def self.new(&blk)
6
+ Template.new(&blk)
7
+ end
8
+
9
+ class Template
10
+ def initialize(&blk)
11
+ @blk = blk
12
+ end
13
+
14
+ def to_s
15
+ target = String.new
16
+ renderer = Renderer.new(target)
17
+ render_with(renderer)
18
+ target
19
+ end
20
+
21
+ def to_html
22
+ to_s
23
+ end
24
+
25
+ def render_with(renderer)
26
+ @blk.call(renderer)
27
+ end
28
+ end
29
+
30
+ class Renderer
31
+ def initialize(target)
32
+ @target = target
33
+ end
34
+
35
+ def <<(obj)
36
+ obj = obj.to_tubby if obj.respond_to?(:to_tubby)
37
+ if obj.is_a?(Tubby::Template)
38
+ obj.render_with(self)
39
+ elsif obj.respond_to?(:to_html)
40
+ @target << obj.to_html
41
+ elsif obj.respond_to?(:html_safe?) && obj.html_safe?
42
+ @target << obj
43
+ else
44
+ @target << CGI.escape_html(obj.to_s)
45
+ end
46
+ self
47
+ end
48
+
49
+ def raw!(text)
50
+ @target << text.to_s
51
+ end
52
+
53
+ def doctype!
54
+ @target << "<!DOCTYPE html>"
55
+ end
56
+
57
+ def __attrs!(attrs)
58
+ attrs.each do |key, value|
59
+ if value.is_a?(Array)
60
+ value = value.compact.join(" ")
61
+ end
62
+
63
+ if value
64
+ key = key.to_s.tr("_", "-")
65
+
66
+ if value == true
67
+ @target << " #{key}"
68
+ else
69
+ value = CGI.escape_html(value.to_s)
70
+ @target << " #{key}=\"#{value}\""
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def tag!(name, content = nil, **attrs)
77
+ @target << "<" << name
78
+ __attrs!(attrs)
79
+ @target << ">"
80
+ self << content if content
81
+ yield if block_given?
82
+ @target << "</" << name << ">"
83
+ end
84
+
85
+ def self_closing_tag!(name, **attrs)
86
+ @target << "<" << name
87
+ __attrs!(attrs)
88
+ @target << ">"
89
+ end
90
+
91
+ TAGS = %w[
92
+ a abbr acronym address applet article aside audio b basefont bdi bdo big
93
+ blockquote body button canvas caption center cite code colgroup datalist
94
+ dd del details dfn dir div dl dt em fieldset figcaption figure font footer
95
+ form frame frameset h1 h2 h3 h4 h5 h6 head header hgroup html i iframe ins
96
+ kbd label legend li map mark math menu meter nav
97
+ object ol optgroup option output p pre progress q rp rt ruby s samp
98
+ section select small span strike strong style sub summary sup svg table
99
+ tbody td textarea tfoot th thead time title tr tt u ul var video xmp
100
+ ]
101
+
102
+ SELF_CLOSING_TAGS = %w[
103
+ base link meta hr br wbr img embed param source track area col input
104
+ keygen command
105
+ ]
106
+
107
+ TAGS.each do |name|
108
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
109
+ def #{name}(content = nil, **attrs, &blk)
110
+ tag!(#{name.inspect}, content, attrs, &blk)
111
+ end
112
+ RUBY
113
+ end
114
+
115
+ SELF_CLOSING_TAGS.each do |name|
116
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
117
+ def #{name}(**attrs)
118
+ self_closing_tag!(#{name.inspect}, attrs)
119
+ end
120
+ RUBY
121
+ end
122
+
123
+ def script(content = nil, **attrs)
124
+ @target << "<script"
125
+ __attrs!(attrs)
126
+ @target << ">"
127
+ if content
128
+ if content =~ /<(!--|script|\/script)/
129
+ raise "script tags can not contain #$&"
130
+ end
131
+ @target << content
132
+ end
133
+ @target << "</script>"
134
+ end
135
+ end
136
+ end
137
+
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tubby
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Magnus Holm
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - judofyr@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE.md
22
+ - README.md
23
+ - lib/tubby.rb
24
+ homepage: https://github.com/judofyr/tubby
25
+ licenses:
26
+ - 0BSD
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.6.11
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: HTML templates as Ruby
48
+ test_files: []