htmless 0.4

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.
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
+