shaven 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +12 -0
  2. data/.rspec +1 -0
  3. data/Isolate +12 -0
  4. data/README.md +183 -0
  5. data/Rakefile +47 -0
  6. data/lib/shaven.rb +27 -0
  7. data/lib/shaven/core_ext/hash.rb +22 -0
  8. data/lib/shaven/core_ext/object.rb +17 -0
  9. data/lib/shaven/document.rb +9 -0
  10. data/lib/shaven/helpers/html.rb +68 -0
  11. data/lib/shaven/nokogiri_ext/node.rb +86 -0
  12. data/lib/shaven/presenter.rb +90 -0
  13. data/lib/shaven/scope.rb +50 -0
  14. data/lib/shaven/transformer.rb +109 -0
  15. data/lib/shaven/transformers/auto.rb +17 -0
  16. data/lib/shaven/transformers/condition.rb +25 -0
  17. data/lib/shaven/transformers/context.rb +35 -0
  18. data/lib/shaven/transformers/dummy.rb +22 -0
  19. data/lib/shaven/transformers/list.rb +51 -0
  20. data/lib/shaven/transformers/reverse_condition.rb +25 -0
  21. data/lib/shaven/transformers/text_or_node.rb +42 -0
  22. data/lib/shaven/version.rb +13 -0
  23. data/shaven.gemspec +20 -0
  24. data/spec/benchmarks.rb +189 -0
  25. data/spec/fixtures/condition.html +5 -0
  26. data/spec/fixtures/context.html +7 -0
  27. data/spec/fixtures/dummy.html +4 -0
  28. data/spec/fixtures/list.html +6 -0
  29. data/spec/fixtures/list_of_contexts.html +6 -0
  30. data/spec/fixtures/reverse_condition.html +5 -0
  31. data/spec/fixtures/text_or_node.html +4 -0
  32. data/spec/helpers_html_spec.rb +54 -0
  33. data/spec/nokogiri_node_spec.rb +55 -0
  34. data/spec/presenter_spec.rb +36 -0
  35. data/spec/spec_helper.rb +11 -0
  36. data/spec/transformers/auto_spec.rb +34 -0
  37. data/spec/transformers/condition_spec.rb +28 -0
  38. data/spec/transformers/context_spec.rb +25 -0
  39. data/spec/transformers/dummy_spec.rb +18 -0
  40. data/spec/transformers/list_spec.rb +41 -0
  41. data/spec/transformers/reverse_condition_spec.rb +28 -0
  42. data/spec/transformers/text_or_node_spec.rb +65 -0
  43. metadata +123 -0
@@ -0,0 +1,90 @@
1
+ require 'shaven/scope'
2
+ require 'shaven/document'
3
+ require 'shaven/transformer'
4
+ require 'shaven/helpers/html'
5
+
6
+ module Shaven
7
+ # Presenters are placeholder for all logic which is going to fill in your html
8
+ # templates. Remember that presenters shouldn't contain any raw HTML code,
9
+ # you can generate it using html helpers (see <tt>Shaven::Helpers::HTML</tt>
10
+ # for details).
11
+ #
12
+ # ==== Simple example
13
+ #
14
+ # class DummyPreseneter < Shaven::Presenter
15
+ # def title
16
+ # "Hello world!"
17
+ # end
18
+ # end
19
+ #
20
+ # presenter = DummyPreseneter.feed("<!DOCTYPE html><html><body><h1 rb="title">Example...</h1></body><html>")
21
+ # presenter.to_html # => ...
22
+ #
23
+ # ==== DOM Manipulation
24
+ #
25
+ # If your presenter method has one argument, then related DOM node will be passed
26
+ # to it while transformation process. You can use it to change its attributes or
27
+ # content, replace it with other node or text, remove it, etc.
28
+ #
29
+ # class DummyPresenter < Shaven::Presenter
30
+ # def title(node)
31
+ # node.update!(:id => "title") { "Hello world!" }
32
+ # end
33
+ # end
34
+ #
35
+ # ==== HTML helpers
36
+ #
37
+ # Shaven's presenters provides set of helpers to generate extra html nodes. Take a look
38
+ # at example:
39
+ #
40
+ # class DummyPresenter < Shaven::Presenter
41
+ # def login_link
42
+ # a(:href => login_path) { "Login to your account!" }
43
+ # end
44
+ #
45
+ # def title
46
+ # tag(:h1, :id => "title") { "Hello world!" }
47
+ # end
48
+ # end
49
+ #
50
+ class Presenter
51
+ include Helpers::HTML
52
+
53
+ class << self
54
+ def feed(tpl)
55
+ new(Document.new(tpl))
56
+ end
57
+
58
+ def render(tpl, context={})
59
+ feed(tpl).render(context)
60
+ end
61
+ end
62
+
63
+ attr_reader :scope
64
+
65
+ def initialize(document)
66
+ @document = document
67
+ @scope = Scope.new(self)
68
+ end
69
+
70
+ def render(context={})
71
+ unless compiled?
72
+ @scope.unshift(context.stringify_keys) unless context.empty?
73
+ Transformer.apply!(@scope.with(@document.root))
74
+ @compiled = true
75
+ end
76
+
77
+ @document.to_html
78
+ end
79
+ alias_method :to_html, :render
80
+
81
+ def compiled?
82
+ !!@compiled
83
+ end
84
+
85
+ # Some tricks to make presenter acts as array :)
86
+
87
+ alias_method :key?, :respond_to?
88
+ alias_method :[], :method
89
+ end # Presenter
90
+ end # Shaven
@@ -0,0 +1,50 @@
1
+ module Shaven
2
+ # Special kind of array to fetch data from stack of context scopes.
3
+ #
4
+ # ==== Example
5
+ #
6
+ # scope = Scope.new({:foo => nil})
7
+ # scope["foo"] # => nil
8
+ # scope.unshift({"foo" => "Bar!", "spam" => "Eggs!"})
9
+ # scope["foo"] # => "Bar!"
10
+ # scope.unshift({"foo" => "Foobar!"})
11
+ # scope["foo"] # => "Foobar!"
12
+ #
13
+ class Scope < Array
14
+ # DOM node wrapped by this scope.
15
+ attr_reader :node
16
+
17
+ def initialize(*args)
18
+ super(args)
19
+ end
20
+
21
+ def [](key)
22
+ each { |scope|
23
+ if scope.key?(key = key.to_s)
24
+ value = scope[key]
25
+
26
+ if value.is_a?(Proc) or value.is_a?(Method)
27
+ args = [node, self]
28
+ return value.call(*args.take(value.arity))
29
+ else
30
+ return value
31
+ end
32
+ end
33
+ }
34
+ end
35
+
36
+ # Assigns given DOM node to this context and returns itself. This method will
37
+ # be used to fast switch from one node into another while transformation process.
38
+ #
39
+ # ==== Example
40
+ #
41
+ # node_scope = scope.with(node)
42
+ # node_scope.unshift({"foo" => proc { |node| node.update! :id => "hello-node" }})
43
+ # node_scope["foo"] # => node
44
+ #
45
+ def with(node)
46
+ @node = node
47
+ return self
48
+ end
49
+ end # Scope
50
+ end # Shaven
@@ -0,0 +1,109 @@
1
+ module Shaven
2
+ class Transformer
3
+ # Just sugar to not using <tt>self</tt> in inheritance definition.
4
+ Base = self
5
+
6
+ # Load all built-in transformers
7
+ Dir[File.dirname(__FILE__)+"/transformers/*.rb"].each { |transformer|
8
+ require transformer
9
+ }
10
+
11
+ # This is chain containing list of DOM caller arguments and related transformers.
12
+ # All of them will be sequentially applied to each node in your template. Order is
13
+ # important because some of them allow to keep going with other transformations
14
+ # after they are applied, other doesn't allow that.
15
+ CALLERS = [
16
+ ['dummy', Dummy],
17
+ ['if', Condition],
18
+ ['unless', ReverseCondition],
19
+ [nil, Auto]
20
+ ]
21
+
22
+ class << self
23
+ # Returns list of callers with full names. Names are combined with configuration
24
+ # setting in <tt>Shaven.caller_key</tt>.
25
+ def callers
26
+ @callers ||= CALLERS.map { |(name,trans)|
27
+ name = [Shaven.caller_key, name].compact.join(':')
28
+ [name,trans]
29
+ }
30
+ end
31
+
32
+ # Goes through callers chain and tries to apply all matching transformers within
33
+ # given scope. If scope for children should be combined/modified then it returns
34
+ # new scope for later usage, otherwise +nil+ will be returned.
35
+ def apply_each(scope, &block)
36
+ callers.each { |(name,trans)|
37
+ if key = scope.node.delete(name)
38
+ value = trans.find_value(scope, key).to_shaven
39
+
40
+ if trans.can_be_transformed?(value)
41
+ transformer = trans.new(key, value, scope)
42
+ extra_scope = transformer.transform!
43
+ return extra_scope unless transformer.allow_continue?
44
+ end
45
+ end
46
+ }
47
+ nil
48
+ end
49
+
50
+ # Applies each transformers within scope, to all children of represented document's
51
+ # node. Transformations are applied only for element nodes.
52
+ def apply!(scope)
53
+ scope.node.children.each { |child|
54
+ if child.elem?
55
+ extra_scope = apply_each(scope.with(child))
56
+ scope.unshift(extra_scope) if extra_scope
57
+ apply!(scope)
58
+ scope.shift if extra_scope
59
+ end
60
+ }
61
+ nil
62
+ end
63
+
64
+ # This method contains set of conditions which tells if given value can be
65
+ # used to transformation on current node. Each transformer should override
66
+ # this method if has such extra requirements.
67
+ def can_be_transformed?(value)
68
+ true
69
+ end
70
+
71
+ # Transformers can load values from scope in differen ways, so this method
72
+ # allows to define such own way within each trasformer. By default it just
73
+ # picks up specified key from given scope.
74
+ def find_value(scope, key)
75
+ scope[key]
76
+ end
77
+ end # self
78
+
79
+ # Name of the presenters method/key from which value has been extracted.
80
+ attr_reader :name
81
+ # Value extracted from presenter.
82
+ attr_reader :value
83
+ # Transformation context scope.
84
+ attr_reader :scope
85
+
86
+ def initialize(name, value, scope)
87
+ @name, @value, @scope = name, value, scope
88
+ end
89
+
90
+ # Just shortcut for <tt>scope.node</tt>.
91
+ def node
92
+ scope.node
93
+ end
94
+
95
+ # If this method returns +true+ then transformers left in chain gonna be applied
96
+ # to node within current scope, otherwise transformation chain will be broken.
97
+ # By default continuing after one transformation is not allowed.
98
+ def allow_continue?
99
+ false
100
+ end
101
+
102
+ # This method should contain all transformation directives. If scope for children
103
+ # nodes should be modified then method should return extra hash/scope which will
104
+ # be temporary combined with current one, otherwise returns +nil+.
105
+ def transform!
106
+ raise NotImplementedError, "You have to implement #transform! in your transformer"
107
+ end
108
+ end # Transformer
109
+ end # Shaven
@@ -0,0 +1,17 @@
1
+ module Shaven
2
+ class Transformer
3
+ # Its job is to automatically recognize which transformer should be applied
4
+ # using specified settings. Regocnition is done by matching value type.
5
+ class Auto < Base
6
+ def self.new(name, value, scope)
7
+ if Context.can_be_transformed?(value)
8
+ Context.new(name, value, scope)
9
+ elsif List.can_be_transformed?(value)
10
+ List.new(name, value, scope)
11
+ else
12
+ TextOrNode.new(name, value, scope)
13
+ end
14
+ end
15
+ end # Auto
16
+ end # Transformer
17
+ end # Shaven
@@ -0,0 +1,25 @@
1
+ module Shaven
2
+ class Transformer
3
+ # This transformer applies conditional operations to nodes. It applies to
4
+ # all nodes containing <tt>rb:if</tt> attribute.
5
+ #
6
+ # See Also: <tt>Shaven::Transformer::ReverseCondition</tt>
7
+ #
8
+ # ==== Example
9
+ #
10
+ # <div rb:if="logged_in?">
11
+ # Hello <span rb="user_name">John Doe</span>!
12
+ # </div>
13
+ #
14
+ class Condition < Base
15
+ def allow_continue?
16
+ !!value
17
+ end
18
+
19
+ def transform!
20
+ node.remove unless value
21
+ nil
22
+ end
23
+ end # Condition
24
+ end # Transformer
25
+ end # Shaven
@@ -0,0 +1,35 @@
1
+ module Shaven
2
+ class Transformer
3
+ # This transformer Can be applied only when value is an instance of +Hash+,
4
+ # <tt>Shaven::Scope</tt>, or <tt>Shaven::Preseneter</tt>. It doesn't modify
5
+ # anything within given node, but modifies context for childrens.
6
+ #
7
+ # ==== Example
8
+ #
9
+ # <div rb="user">
10
+ # <div rb="name">John Doe</div>
11
+ # <div rb="email">email@example.com</div>
12
+ # </div>
13
+ #
14
+ # applied with given value:
15
+ #
16
+ # { :name => "Marty Macfly", :email => "marty@macf.ly" }
17
+ #
18
+ # ... generates:
19
+ #
20
+ # <div>
21
+ # <div>Marty Macfly</div>
22
+ # <div>marty@macf.ly</div>
23
+ # </div>
24
+ #
25
+ class Context < Base
26
+ def self.can_be_transformed?(value)
27
+ value.is_a?(::Hash)
28
+ end
29
+
30
+ def transform!
31
+ value.stringify_keys
32
+ end
33
+ end # Context
34
+ end # Transformer
35
+ end # Shaven
@@ -0,0 +1,22 @@
1
+ module Shaven
2
+ class Transformer
3
+ # It removes all nodes containing <tt>rb:dummy</tt> attribute. It's very
4
+ # usefull when your template contains lot of example items. Instead of deleting
5
+ # them manualy you can mark them as dummy, so your designer can in the future
6
+ # edit templates directly in application.
7
+ #
8
+ # ==== Example
9
+ #
10
+ # <ul id="emperors">
11
+ # <li rb="emperors">Karol the Great</li>
12
+ # <li rb:dummy="yes">Julius Cesar</li>
13
+ # <li rb:dummy="yes">Alexander the Great</li>
14
+ # <ul>
15
+ #
16
+ class Dummy < Base
17
+ def transform!
18
+ node.remove
19
+ end
20
+ end # Dummy
21
+ end # Transformer
22
+ end # Shaven
@@ -0,0 +1,51 @@
1
+ module Shaven
2
+ class Transformer
3
+ # This transformer can be applied when value can be iterated (responds to <tt>#each</tt>).
4
+ # It treats given node as template so generates sequence of clones for each list value,
5
+ # and finally removes original node.
6
+ #
7
+ # ==== Example
8
+ #
9
+ # <ul id="users">
10
+ # <li rb="users">John Doe</li>
11
+ # </ul>
12
+ #
13
+ # applied with given value:
14
+ #
15
+ # ["Emmet Brown", "Marty Macfly", "Biff Tannen"]
16
+ #
17
+ # ... generates:
18
+ #
19
+ # <ul id="users">
20
+ # <li>Emmet Brown</li>
21
+ # <li>Marty Macfly</li>
22
+ # <li>Biff Tannen</li>
23
+ # </ul>
24
+ #
25
+ class List < Base
26
+ def self.can_be_transformed?(value)
27
+ value.is_a?(Array)
28
+ end
29
+
30
+ def transform!
31
+ array_scope = {}
32
+ parent = node.parent
33
+ id = 0
34
+
35
+ value.each { |item|
36
+ new_node = node.dup
37
+ array_scope["__shaven_list_item_#{id}"] = item
38
+ new_node['rb'] = "__shaven_list_item_#{id}"
39
+ parent.add_child(new_node)
40
+ id += 1
41
+ }
42
+
43
+ node.remove
44
+ array_scope = scope.dup.unshift(array_scope)
45
+ self.class.apply!(array_scope.with(parent))
46
+
47
+ nil
48
+ end
49
+ end # List
50
+ end # Transformer
51
+ end # Shaven
@@ -0,0 +1,25 @@
1
+ module Shaven
2
+ class Transformer
3
+ # This transformer applies reverse conditional operations to nodes. It applies
4
+ # to all nodes containing <tt>rb:unless</tt> attribute.
5
+ #
6
+ # See Also: <tt>Shaven::Transformer::Condition</tt>
7
+ #
8
+ # ==== Example
9
+ #
10
+ # <div rb:unless="logged_in?">
11
+ # <a href="#" rb="login_link">Login to your account!</a>
12
+ # </div>
13
+ #
14
+ class ReverseCondition < Base
15
+ def allow_continue?
16
+ !value
17
+ end
18
+
19
+ def transform!
20
+ node.remove if value
21
+ nil
22
+ end
23
+ end # ReverseCondition
24
+ end # Transformer
25
+ end # Shaven
@@ -0,0 +1,42 @@
1
+ module Shaven
2
+ class Transformer
3
+ # This transformers is applied to any kind of value which not applies to any
4
+ # other transformer. The only requirement is to node contains <tt>rb</tt> attribute.
5
+ # If result is different than current node object then will be assigned as its
6
+ # content, otherwise nothin will happen (any updates goes in presenter then).
7
+ #
8
+ # === Example
9
+ #
10
+ # <h1 rb="title">Example</h1>
11
+ # <p rb="description">Lorem ipsum dolor...</p>
12
+ #
13
+ # with given presenter:
14
+ #
15
+ # class MyPresenter < Shaven::Presenter
16
+ # def title
17
+ # "Hello world!"
18
+ # end
19
+ #
20
+ # def description(node)
21
+ # node.update!(:id => "description") { "World is beautiful!" }
22
+ # end
23
+ # end
24
+ #
25
+ # ... generates:
26
+ #
27
+ # <h1>Hello world!</h1>
28
+ # <p id="description">World is beautiful!</p>
29
+ #
30
+ class TextOrNode < Base
31
+ def transform!
32
+ if value.nokogiri_node?
33
+ node.inner_html = value unless value === node
34
+ else
35
+ node.content = value.to_s
36
+ end
37
+
38
+ nil
39
+ end
40
+ end # TextOrNode
41
+ end # Transformer
42
+ end # Shaven