shaven 0.0.1

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 (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