tubby 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.
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: []