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