htmless 0.4

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 Petr Chalupa <git@pitr.ch>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ ## Htmless
2
+
3
+ Fast extensible html5 builder in pure Ruby
4
+
5
+ - Documentation: <http://blog.pitr.ch/htmless>
6
+ - Source: <https://github.com/pitr-ch/htmless>
7
+ - Blog: <http://blog.pitr.ch/blog/categories/htmless/>
data/README_FULL.md ADDED
@@ -0,0 +1,585 @@
1
+ # Htmless
2
+
3
+ Fast extensible html5 builder in pure Ruby
4
+
5
+ - Documentation: <http://blog.pitr.ch/htmless>
6
+ - Source: <https://github.com/pitr-ch/htmless>
7
+ - Blog: <http://blog.pitr.ch/blog/categories/htmless/>
8
+
9
+ ## Why?
10
+
11
+ I needed html builder with these characteristics:
12
+
13
+ * Ruby
14
+ * Fast
15
+ * Extensibility for each tag
16
+
17
+ Disadvanteges of other options:
18
+
19
+ * Erector - quite high level, no tag extensibility
20
+ * Markaby - slow
21
+ * Wee::Brush - extensible but not a standalone gem
22
+ * Tagz - very slow
23
+ * Erubis - fast but temlate engine and no tag extensibility
24
+ * Tenjin - faster but temlate engine and no tag extensibility
25
+
26
+ ## Quick syntax example
27
+
28
+ !!!ruby
29
+ Htmless::Formatted.new.go_in do
30
+ html5
31
+ html do
32
+ head { title 'my_page' }
33
+ body do
34
+ div.content! do
35
+ p.centered "my page's content"
36
+ end
37
+ end
38
+ end
39
+ end.to_html
40
+
41
+ returns
42
+
43
+ !!!html
44
+ <!DOCTYPE html>
45
+ <html xmlns="http://www.w3.org/1999/xhtml">
46
+ <head>
47
+ <title>my_page</title>
48
+ </head>
49
+ <body>
50
+ <div id="content">
51
+ <p class="centered">my page's content</p>
52
+ </div>
53
+ </body>
54
+ </html>
55
+
56
+ ## Chaining
57
+
58
+ !!!ruby
59
+ div.class('left').id('menu').class('border').onclick('run();').with do
60
+ text 'hello'
61
+ end
62
+
63
+ prints
64
+
65
+ !!!html
66
+ <div class="left border" id="menu" onclick="run();">hello</div>
67
+
68
+ Content block must be last in the chain. No other calls are allowed after block.
69
+
70
+ !!!ruby
71
+ div do
72
+ text 'content'
73
+ end.id('an_id') # won't work
74
+
75
+ ## Attributes
76
+
77
+ !!!ruby
78
+ div :id => 'menu', :class => 'left' # is shortcut for:
79
+ div.attributes :id => 'menu', :class => 'left'
80
+
81
+ * `#attributes` calls underlining methods `#id` and `#class`
82
+ * If you extend tag by a method, you can call it through `#attributes` or `#attribute`
83
+
84
+ Any attribute method you call is immediately appended to output with exception of classes. They are acumulating
85
+ until tag is closed.
86
+
87
+ !!!ruby
88
+ div(:class => 'left').class('center') # <div class='left center'></div>
89
+ div(:id => 1).id(2) # <div id="1" id="2"></div>
90
+
91
+ Udefined attribute can be rendered with.
92
+
93
+ !!!ruby
94
+ html.attribute :xmlns, 'http://www.w3.org/1999/xhtml'
95
+ # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
96
+
97
+ If `#attribute` finds defined method for desired attribute, the method is called.
98
+
99
+ !!!ruby
100
+ div.attribute :class => 'left' # is equvivalent to:
101
+ div.class 'left'
102
+
103
+ ## Content, Boolean attributes
104
+
105
+ !!!ruby
106
+ div 'content' # <div>content</div>
107
+ div.content 'content' # <div>content</div>
108
+ div :content => 'content' # <div>content</div>
109
+ div { text 'content' } # <div>content</div>
110
+ div.with { text 'content' } # <div>content</div>
111
+ div 'content', :id => :id # <div id="id">content</div>
112
+ div(:id => :id) { text 'content' } # <div id="id">content</div>
113
+ div.id :id do
114
+ text 'content'
115
+ end # <div id="id">content</div>
116
+
117
+ ### Boolean attributes
118
+
119
+ Attributes like `checked` and `disabled` test value for true, if false they render nothing
120
+
121
+ !!!ruby
122
+ input.disabled am_i_disabled?
123
+ # => <input disabled="disabled" />
124
+ # or <input />
125
+
126
+ ## Id, class and mimic
127
+
128
+ !!!ruby
129
+ div.menu!.left.hidden # => <div id="menu" class="left hidden"></div>
130
+
131
+ Content can be passed in the usual way:
132
+
133
+ !!!ruby
134
+ div.menu! 'content' # => <div id="menu">content</div>
135
+ div.menu! { p 'content' } # => <div id="menu"><p>content</p></div>
136
+
137
+ `#class` accepts an array, values are joined with spaces and `false`, `nil` values are ignored
138
+
139
+ !!!ruby
140
+ div.class('menu', 'big', hide? && 'hidden')
141
+ # => <div class="menu big hidden"></div>
142
+ # or <div class="menu big"></div>
143
+
144
+ `#id` accepts also an array, values are joined with '-' and `false`, `nil` values are ignored
145
+
146
+ !!!ruby
147
+ div.id('menu', 'big', a_failing_test && 'useless') # => <div id="menu-big"></div>
148
+
149
+ Object (model) can be used to set class and id
150
+
151
+ !!!ruby
152
+ user = User.new(:id => 1)
153
+ div(:class => 'model')[user].with { text 'data' }
154
+ # => <div id="user-1" class="model user">data</div>
155
+
156
+ `#mimic` which is aliased as `#[]` looks for `.htmless_ref` or it uses the class name to render the class part.
157
+ As id is used already determined class and `#htmless_ref`, `#id`, `#object_id` which one is found first.
158
+
159
+ ## Data attributes, Join
160
+
161
+ ### Data attributes
162
+
163
+ !!!ruby
164
+ div.data_secret("I won't tell.") # => <div data-secret="I won't tell."></div>
165
+ data :secret => "I won't tell." # => <div data-secret="I won't tell."></div>
166
+
167
+
168
+ ### Join
169
+
170
+ `#join` enables easy rendering of collections
171
+
172
+ !!!ruby
173
+ join([1, 1.2], ->{ text ', ' }) {|n| b "#{n}cm" }
174
+ # => "<b>1cm</b>, <b>1.2cm</b>"
175
+ join([1, 1.2], ', ') {|n| b "#{n}cm" }
176
+ # => "<b>1cm</b>, <b>1.2cm</b>"
177
+ join([1, ->{ text 'missing' }], ', ') {|n| b "#{n}cm" }
178
+ # => "<b>1cm</b>, missing"
179
+
180
+ A block in the collection is rendered directly without iterator. This can be useful when menu with some delimiters
181
+ is rendered based on collection of some objects and you need to add one or more untypical menu items.
182
+
183
+ ## '_' vs '-'
184
+
185
+ '_' in attributes is transformed to '-'
186
+
187
+ !!!ruby
188
+ meta.http_equiv 'Content-Type' # => <meta http-equip="Content-Type" />
189
+
190
+ '_' in class shortcut methods is transformed to '-'
191
+
192
+ !!!ruby
193
+ div.a_class.an_id! # => <div id="an-id" class="a-class"></div>
194
+
195
+ Ids generated by Arrays are joined with '-'
196
+
197
+ !!!ruby
198
+ div.id('an', 'id') # => <div id="an-id"></div>
199
+
200
+
201
+ ## Tag's reprezentation
202
+
203
+ Each tag has its own class.
204
+
205
+ !!!ruby
206
+ div.rclass # => #<Class:0x00000001d449b8(Htmless::Formatted.dc[:Div])>
207
+ li.rclass # => #<Class:0x00000001d449b8(Htmless::Formatted.dc[:Li])>
208
+
209
+ `#rclass` is original ruby method `#class`
210
+
211
+
212
+ ## Getting a builder
213
+
214
+ Creating new builder is relatively expensive. There is a pool of builders implemented.
215
+
216
+ !!!ruby
217
+ if am_i_smart?
218
+ pool = Htmless::Pool.new Htmless::Formatted # store pool somewhere globalish for reuse
219
+ builder = pool.get # => new builder from pool if there is one, or a newlly created one
220
+ # ... do yours stuff
221
+ builder.release # resets builder and returns it to the pool
222
+ else
223
+ b = Htmless::Formatted.new
224
+ # ... do yours stuff
225
+ # later on builder gets garbage collected
226
+ end
227
+
228
+ Be careful not to use builder after you have released it.
229
+
230
+ !!!ruby
231
+ if am_i_freaking_smart?
232
+ pool = Htmless::Pool.new Htmless::Formatted
233
+ xhtml = pool.get.go_in(your_data) do |data|
234
+ # render your data ...
235
+ end.to_html! # returns xhtml and releases the builder
236
+ end
237
+
238
+ This way builder doesn't get stored anywhere.
239
+
240
+ ## How to use
241
+
242
+ The idea is that any object intended to rendering will have methods which renders the object into builder.
243
+ There is a `Htmless::Helper` and method `#render` (also aliased as `#r`) for that purpose.
244
+
245
+ !!!ruby
246
+ class User < Struct.new(:name, :login, :email)
247
+ extend Htmless::Helper
248
+
249
+ builder :detail do |user|
250
+ ul do
251
+ r user, :attribute, :name
252
+ r user, :attribute, :login
253
+ r user, :attribute, :email
254
+ end
255
+ end
256
+
257
+ def attribute(b, attribute)
258
+ b.li do
259
+ b.strong "#{attribute}: "
260
+ b.text self.send(attribute)
261
+ end
262
+ end
263
+ end
264
+
265
+ `.builder` is just shortcut to define method `User#detail` like this:
266
+
267
+ !!!ruby
268
+ def detail(b)
269
+ b.go_in(self) do |user| # this block is the same as the one passed
270
+ ul do # above to .builder
271
+ r user, :attribute, :name
272
+ r user, :attribute, :login
273
+ r user, :attribute, :email
274
+ end
275
+ end
276
+ end
277
+
278
+ is same as
279
+
280
+ !!!ruby
281
+ builder :detail do |user|
282
+ ul do
283
+ r user, :attribute, :name
284
+ r user, :attribute, :login
285
+ r user, :attribute, :email
286
+ end
287
+ end
288
+
289
+ and
290
+
291
+ !!!ruby
292
+ user = User.new("Peter", "peter", "peter@example.com")
293
+ pool.get.dive do
294
+ r user, :detail
295
+ end.to_html!
296
+
297
+ returns:
298
+
299
+ !!!html
300
+ <ul>
301
+ <li>
302
+ <strong>name: </strong>Peter
303
+ </li>
304
+ <li>
305
+ <strong>login: </strong>peter
306
+ </li>
307
+ <li>
308
+ <strong>email: </strong>peter@example.com
309
+ </li>
310
+ </ul>
311
+
312
+ ## Contexts
313
+
314
+ Html can be rendered outside of builder's context
315
+
316
+ !!!ruby
317
+ class User
318
+ attr_reder :name, :age
319
+ def detail(b) # builder
320
+ b.ul { b.li name; b.li name }
321
+ end
322
+ end
323
+
324
+ or `#go_in` (also aliased as `#dive`) can be used to get into builder's context
325
+
326
+ !!!ruby
327
+ class User
328
+ extend Htmless::Helper
329
+ attr_reder :name, :age
330
+ builder :detail do |user|
331
+ ul { li user.name; li user.age }
332
+ end
333
+ end
334
+ # => <ul><li>john Doe</li><li>25</li></ul>
335
+
336
+ ## Helpers
337
+
338
+ If they are needed they can be mixed directly into Builder's instance
339
+
340
+ !!!ruby
341
+ Htmless::Formatted.new.go_in do
342
+ extend ActionView::Helpers::NumberHelper
343
+ div number_with_precision(Math::PI, :precision => 4)
344
+ end.to_html # => <div>3.1416</div>
345
+
346
+ *Be careful when you are using this with `Pool`. Some instances may have helpers and some don't.*
347
+
348
+ Or new builder descendant can be made.
349
+
350
+ !!!ruby
351
+ class MyBuilder < Hammer::FormattedBuilder
352
+ include ActionView::Helpers::NumberHelper
353
+ end
354
+
355
+ MyBuilder.new.go_in do
356
+ div number_with_precision(Math::PI, :precision => 4)
357
+ end.to_html # => <div>3.1416</div>
358
+
359
+ ## Implementation details - Tag's shared instances
360
+
361
+ There are no multiple instances for each tag.
362
+ Every tag of the same type share a same instance (unique within the instance of a builder).
363
+
364
+ !!!ruby
365
+ puts(pool.get.go_in do
366
+ puts div.object_id
367
+ puts div.object_id
368
+ end.to_html!)
369
+ # =>
370
+ # 10069200
371
+ # 10069200
372
+ # <div></div><div></div>
373
+
374
+ `Htmless` creates what he can prior to rendering and uses heavily meta-programming, because of that instantiating
375
+ the very first instance of builder triggers some magic staff taking about a one second. Creating new builders of the
376
+ same class is than much faster and getting builder from a pool is instant.
377
+
378
+ This won't work:
379
+
380
+ !!!ruby
381
+ puts(pool.get.go_in do
382
+ a = div 'a'
383
+ div 'b'
384
+ a.class 'class'
385
+ end.to_html!)
386
+ # => <div>a</div><div class="class">b</div>
387
+
388
+ because when `#class` is called the second div is being built.
389
+
390
+ ## Implementation details - DynamicClasses
391
+
392
+ !!!ruby
393
+ class Parent
394
+ class LeftEye
395
+ def to_s
396
+ 'left eye'
397
+ end
398
+ end
399
+ class RightEye < LeftEye
400
+ def to_s
401
+ 'next to ' + super
402
+ end
403
+ end
404
+ end
405
+ class AChild < Parent
406
+ end
407
+ class AMutant
408
+ class LeftEye < superclass::LeftEye
409
+ def to_s
410
+ 'laser ' + super
411
+ end
412
+ end
413
+ end
414
+
415
+ How to define `AMutant::RihtEye` to return `"next to laser left eye"` ?
416
+
417
+ !!!ruby
418
+ class Parent
419
+ extend DynamicClasses
420
+ dynamic_classes do
421
+ def_class :LeftEye do
422
+ def to_s; 'left eye'; end
423
+ end
424
+ def_class :RightEye, :LeftEye do
425
+ class_eval <<-RUBYCODE, __FILE__, __LINE__+1
426
+ def to_s; 'next to ' + super; end
427
+ RUBYCODE
428
+ end
429
+ end
430
+ end
431
+
432
+ class AChild < Parent
433
+ end
434
+
435
+ class AMutant < Parent
436
+ dynamic_classes do
437
+ extend_class :LeftEye do
438
+ def to_s; 'laser ' + super; end
439
+ end
440
+ end
441
+ end
442
+
443
+ Each class is a diferent object.
444
+
445
+ !!!ruby
446
+ Parent.dynamic_classes[:LeftEye] # => #<Class:0x00000001d449b8(A.dc[:LeftEye])>
447
+ AChild.dynamic_classes[:LeftEye] # => #<Class:0x00000001d42398(A.dc[:LeftEye])>
448
+
449
+ `AMutant.dc[:RightEye]` automaticaly inherits from extended `AMutant.dc[:LeftEye]`
450
+
451
+ !!!ruby
452
+ Parent.dc[:LeftEye].new.to_s # => 'left eye'
453
+ Parent.dc[:RightEye].new.to_s # => 'next to left eye'
454
+
455
+ AChild.dc[:LeftEye].new.to_s # => 'left eye'
456
+ AChild.dc[:RightEye].new.to_s # => 'next to left eye'
457
+
458
+ AMutant.dc[:LeftEye].new.to_s # => 'laser left eye'
459
+ AMutant.dc[:RightEye].new.to_s # => 'next to laser left eye'
460
+
461
+ ## Extensibility
462
+
463
+ !!!ruby
464
+ class MyBuilder < Htmless::Formatted
465
+ dynamic_classes do
466
+ # define new method to all tags
467
+ extend_class :AbstractTag do
468
+ def hide!
469
+ self.class 'hidden'
470
+ end
471
+ end
472
+
473
+ # add pseudo tag
474
+ def_class :Component, :Div do
475
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
476
+ def open(id, attributes = nil, &block)
477
+ super(attributes, &nil).id(id).class('component')
478
+ block ? with(&block) : self
479
+ end
480
+ RUBYCODE
481
+ end
482
+ end
483
+
484
+ define_tag :component
485
+
486
+ # if the class is not needed same can be done this way
487
+ def simple_component(id, attributes = {}, &block)
488
+ div.id(id).attributes attributes, &block
489
+ end
490
+ end
491
+
492
+ MyBuilder.new.go_in do
493
+ div.content!.with do
494
+ span.secret!.class('left').hide!
495
+ component('component-1') do
496
+ strong 'something'
497
+ end
498
+ simple_component 'component-1'
499
+ end
500
+ end.to_html
501
+
502
+ returns
503
+
504
+ !!!html
505
+ <div id="content">
506
+ <span id="secret" class="left hidden"></span>
507
+ <div id="component-1" class="component">
508
+ <strong>something</strong>
509
+ </div>
510
+ <div id="component-1"></div>
511
+ </div>
512
+
513
+ ## Benchmarks
514
+
515
+ ### Synthetic
516
+
517
+ Benchmatk can be found on github. It renders simple page with two collections. 'reuse' means
518
+ that template is precompiled and reused during benchmark.
519
+
520
+ user system total real
521
+ tenjin-reuse 2.040000 0.000000 2.040000 ( 2.055140)
522
+ Htmless::Standard 2.520000 0.000000 2.520000 ( 2.519284)
523
+ fasterubis-reuse 2.580000 0.000000 2.580000 ( 2.581407)
524
+ erubis-reuse 2.680000 0.000000 2.680000 ( 2.690176)
525
+ Htmless::Formatted 2.780000 0.000000 2.780000 ( 2.794307)
526
+ erubis 5.180000 0.000000 5.180000 ( 5.183333)
527
+ fasterubis 5.210000 0.000000 5.210000 ( 5.219176)
528
+ tenjin 7.650000 0.160000 7.810000 ( 7.820490)
529
+ erector 9.450000 0.010000 9.460000 ( 9.471654)
530
+ markaby 14.300000 0.000000 14.300000 ( 14.318844)
531
+ tagz 33.430000 0.000000 33.430000 ( 33.483693)
532
+
533
+ ### Rails 3
534
+
535
+ Benchmatk can be found on github.
536
+ Single page with or without a partial which is rendered 200 times. Partials make no diffrence for Htmless.
537
+
538
+ BenchTest#test_erubis_partials (3.34 sec warmup)
539
+ wall_time: 3.56 sec
540
+ gc_runs: 15
541
+ gc_time: 0.53 ms
542
+ BenchTest#test_erubis_single (552 ms warmup)
543
+ wall_time: 544 ms
544
+ gc_runs: 4
545
+ gc_time: 0.12 ms
546
+ BenchTest#test_htmless (2.33 sec warmup)
547
+ wall_time: 847 ms
548
+ gc_runs: 5
549
+ gc_time: 0.17 ms
550
+ BenchTest#test_tenjin_partial (942 ms warmup)
551
+ wall_time: 1.21 sec
552
+ gc_runs: 7
553
+ gc_time: 0.25 ms
554
+ BenchTest#test_tenjin_single (531 ms warmup)
555
+ wall_time: 532 ms
556
+ gc_runs: 6
557
+ gc_time: 0.20 ms
558
+
559
+ ## Why is it fast?
560
+
561
+ * Optimalization of garbage collecting.
562
+ * 10-15% improvment.
563
+ * Preinicialization (tag's instances, even strings).
564
+ * No string's `#+`, `#{}`. Just `#<<` to buffer.
565
+ * Precomputed spaces for indentation.
566
+ * Doing as less as posible when rendering.
567
+ * Magic by metaprograming not by `method_missing`. Magic is run on inicialization not when rendering.
568
+ * Number of micro optimalization.
569
+ * Data in constants or instance variables.
570
+ * Buffer.
571
+ * No `#define_method`.
572
+ * Method inlining.
573
+ * Probably no real effect :)
574
+
575
+ ## Future plans
576
+
577
+ * compile rendering methods on objects to javascript which will render html form json
578
+ * Helpers for Sinatra, Rails 3
579
+ * Helpers for fragment caching
580
+
581
+ ## Why use it?
582
+
583
+ * Its fast
584
+ * You can use inheritance (imposible with templates) and other goodness of Ruby
585
+ * You can use pure Ruby to write the html
@@ -0,0 +1,43 @@
1
+ # This copies insides of dynamic classes into doc.rb for documenting purposes only
2
+
3
+ require 'pp'
4
+ root = File.expand_path File.dirname(__FILE__)
5
+ $: << root
6
+ require "#{root}/htmless"
7
+
8
+ File.open "#{root}/htmless/doc.rb", 'w' do |out|
9
+ out.write "module Htmless\n"
10
+ out.write " module StubBuilderForDocumentation\n"
11
+
12
+ files = ["#{root}/htmless/abstract/abstract_tag.rb",
13
+ "#{root}/htmless/abstract/abstract_single_tag.rb",
14
+ "#{root}/htmless/abstract/abstract_double_tag.rb"]
15
+ files.each do |file_path|
16
+ source = File.open(file_path, 'r') { |f| f.read }
17
+ source.scan(/def_class\s+(:\w+)(|,\s*(:\w+))\s+do\s+###import(([^#]|#[^#]|##[^#])*)end\s+###import/m) do |match|
18
+ klass = match[0][1..-1]
19
+ parent = match[2] ? match[2][1..-1] : nil
20
+ content = match[3]
21
+
22
+ #content = content.lines.delete_if { |l| l =~ /\#\#\# remove/ }.join("\n")
23
+
24
+ out << " class #{klass}"
25
+ out << " < #{parent}" if parent
26
+ out << "\n"
27
+ out << content
28
+ out << " end\n"
29
+ end
30
+ end
31
+
32
+ #out << " class AbstractTag\n" ff
33
+ #HammerBuilder::GLOBAL_ATTRIBUTES.each do |attr|
34
+ # out << " \#@method #{attr}(value)\n"
35
+ # out << " attribute :#{attr}\n"
36
+ #end
37
+ #out << " end\n"
38
+
39
+ out << " end\n"
40
+ out << "end\n"
41
+ end
42
+
43
+