shaven 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/.rspec +1 -0
- data/Isolate +12 -0
- data/README.md +183 -0
- data/Rakefile +47 -0
- data/lib/shaven.rb +27 -0
- data/lib/shaven/core_ext/hash.rb +22 -0
- data/lib/shaven/core_ext/object.rb +17 -0
- data/lib/shaven/document.rb +9 -0
- data/lib/shaven/helpers/html.rb +68 -0
- data/lib/shaven/nokogiri_ext/node.rb +86 -0
- data/lib/shaven/presenter.rb +90 -0
- data/lib/shaven/scope.rb +50 -0
- data/lib/shaven/transformer.rb +109 -0
- data/lib/shaven/transformers/auto.rb +17 -0
- data/lib/shaven/transformers/condition.rb +25 -0
- data/lib/shaven/transformers/context.rb +35 -0
- data/lib/shaven/transformers/dummy.rb +22 -0
- data/lib/shaven/transformers/list.rb +51 -0
- data/lib/shaven/transformers/reverse_condition.rb +25 -0
- data/lib/shaven/transformers/text_or_node.rb +42 -0
- data/lib/shaven/version.rb +13 -0
- data/shaven.gemspec +20 -0
- data/spec/benchmarks.rb +189 -0
- data/spec/fixtures/condition.html +5 -0
- data/spec/fixtures/context.html +7 -0
- data/spec/fixtures/dummy.html +4 -0
- data/spec/fixtures/list.html +6 -0
- data/spec/fixtures/list_of_contexts.html +6 -0
- data/spec/fixtures/reverse_condition.html +5 -0
- data/spec/fixtures/text_or_node.html +4 -0
- data/spec/helpers_html_spec.rb +54 -0
- data/spec/nokogiri_node_spec.rb +55 -0
- data/spec/presenter_spec.rb +36 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/transformers/auto_spec.rb +34 -0
- data/spec/transformers/condition_spec.rb +28 -0
- data/spec/transformers/context_spec.rb +25 -0
- data/spec/transformers/dummy_spec.rb +18 -0
- data/spec/transformers/list_spec.rb +41 -0
- data/spec/transformers/reverse_condition_spec.rb +28 -0
- data/spec/transformers/text_or_node_spec.rb +65 -0
- 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
|
data/lib/shaven/scope.rb
ADDED
@@ -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
|