hensel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ module Hensel
2
+ class Configuration
3
+ # Define the accessor as boolean method
4
+ def self.attr_boolean_accessor(*keys)
5
+ keys.each do |key|
6
+ attr_accessor key
7
+ define_method("#{key}?"){ !!__send__(key) }
8
+ end
9
+ end
10
+
11
+ attr_boolean_accessor :bootstrap
12
+ attr_boolean_accessor :escape_html
13
+ attr_boolean_accessor :indentation
14
+ attr_boolean_accessor :last_item_link
15
+
16
+ attr_accessor :attr_wrapper, :whitespace, :parent_element, :richsnippet
17
+
18
+ def initialize
19
+ @bootstrap = false
20
+ @escape_html = true
21
+ @indentation = true
22
+ @last_item_link = false
23
+ @richsnippet = :microdata # [:microdata, :rdfa, :nil]
24
+ @attr_wrapper = "'"
25
+ @whitespace = " "
26
+ @parent_element = :ul
27
+ end
28
+
29
+ def [](key)
30
+ instance_variable_get(:"@#{key}")
31
+ end
32
+
33
+ def []=(key, value)
34
+ instance_variable_set(:"@#{key}", value)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ module Hensel
2
+ module Filters
3
+ extend self
4
+
5
+ def register(key, name, block)
6
+ filters[key][name] = block
7
+ end
8
+
9
+ def filters
10
+ @filters ||= { filters: {}, richsnippets: {} }
11
+ end
12
+
13
+ register :richsnippets, :microdata, ->(this){
14
+ return if this.renderer
15
+ this.renderer = ->(that){
16
+ node(:li, options.merge(itemtype: "http://data-vocabulary.org/Breadcrumb", itemscope: true)) do
17
+ if !Hensel.configuration.last_item_link? && item.last?
18
+ node(:span, itemprop: :title){ item.text }
19
+ else
20
+ node(:a, href: item.url, itemprop: :url) do
21
+ node(:span, itemprop: :title){ item.text }
22
+ end
23
+ end
24
+ end
25
+ }
26
+ }
27
+
28
+ register :richsnippets, :rdfa, ->(this){
29
+ return if this.renderer
30
+ append_attribute(:"xmlns:v", "http://rdf.data-vocabulary.org/#", this.parent.options)
31
+ this.renderer = ->(that){
32
+ node(:li, options.merge(typeof: "v:Breadcrumb")) do
33
+ if !Hensel.configuration.last_item_link? && item.last?
34
+ node(:span, property: "v:title"){ item.text }
35
+ else
36
+ node(:a, href: item.url, rel: "v:url", property: "v:title"){ item.text }
37
+ end
38
+ end
39
+ }
40
+ }
41
+
42
+ register :filters, :bootstrap, ->(this){
43
+ append_attribute(:class, "breadcrumb", options)
44
+ append_attribute(:class, "active", items.last.options)
45
+ }
46
+ end
47
+ end
@@ -0,0 +1,113 @@
1
+ module Hensel
2
+ module Helpers
3
+ module TagHelpers
4
+ ESCAPE_VALUES = {
5
+ "'" => "'",
6
+ "&" => "&",
7
+ "<" => "&lt;",
8
+ ">" => "&gt;",
9
+ '"' => "&quot;"
10
+ }
11
+ ESCAPE_REGEXP = Regexp.union(*ESCAPE_VALUES.keys)
12
+ BOOLEAN_ATTRIBUTES = [
13
+ :autoplay,
14
+ :autofocus,
15
+ :formnovalidate,
16
+ :checked,
17
+ :disabled,
18
+ :hidden,
19
+ :loop,
20
+ :multiple,
21
+ :muted,
22
+ :readonly,
23
+ :required,
24
+ :selected,
25
+ :declare,
26
+ :defer,
27
+ :ismap,
28
+ :itemscope,
29
+ :noresize,
30
+ :novalidate
31
+ ]
32
+
33
+ def content_tag(name, content = nil, **options, &block)
34
+ base = tag(name, options)
35
+ content = instance_eval(&block) if !content && block_given?
36
+ if indentation?
37
+ indent = options.fetch(:indent, 0)
38
+ base << "\n"
39
+ base << (content.strip.start_with?("<") ? content : (whitespace(indent + 1)) + content)
40
+ base << "\n"
41
+ base << "#{whitespace(indent)}</#{name}>"
42
+ else
43
+ "#{base}#{content}</#{name}>"
44
+ end
45
+ end
46
+
47
+ def tag(name, indent: 0, **options)
48
+ "#{indentation? ? whitespace(indent) : ""}<#{name}#{tag_attributes(options)}>"
49
+ end
50
+
51
+ def append_attribute(attribute, value, hash)
52
+ if hash[attribute]
53
+ unless hash[attribute].to_s.split(" ").include?(value)
54
+ hash[attribute] = (Array(hash[attribute]) << value) * " "
55
+ end
56
+ else
57
+ hash.merge!(attribute => value)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def tag_attributes(options)
64
+ return '' if options.nil?
65
+ attributes = options.map do |k, v|
66
+ next if v.nil? || v == false
67
+ if v.is_a?(Hash)
68
+ nested_values(k, v)
69
+ elsif BOOLEAN_ATTRIBUTES.include?(k)
70
+ %(#{k}=#{attr_wrapper}#{k}#{attr_wrapper})
71
+ else
72
+ %(#{k}=#{attr_wrapper}#{h(v)}#{attr_wrapper})
73
+ end
74
+ end.compact
75
+ attributes.empty? ? '' : " #{attributes * ' '}"
76
+ end
77
+
78
+ def nested_values(attribute, hash)
79
+ hash.map do |k, v|
80
+ if v.is_a?(Hash)
81
+ nested_values("#{attribute}-#{k.to_s}", v)
82
+ else
83
+ %(#{attribute}-#{k.to_s}=#{attr_wrapper}#{h(v)}#{attr_wrapper}")
84
+ end
85
+ end * ' '
86
+ end
87
+
88
+ def h(string)
89
+ Hensel.configuration.escape_html? ? escape_html(string) : string
90
+ end
91
+
92
+ def escape_html(string)
93
+ string.to_s.gsub(ESCAPE_REGEXP) { |c| ESCAPE_VALUES[c] }
94
+ end
95
+
96
+ def attr_wrapper
97
+ @attr_wrapper ||= Hensel.configuration.attr_wrapper
98
+ end
99
+
100
+ def whitespace(indent = nil)
101
+ if indent.nil?
102
+ @whitespace ||= Hensel.configuration.whitespace
103
+ else
104
+ whitespace * indent
105
+ end
106
+ end
107
+
108
+ def indentation?
109
+ @indentation ||= Hensel.configuration.indentation?
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,9 @@
1
+ require 'hensel/builder'
2
+
3
+ module Hensel
4
+ module Helpers
5
+ def breadcrumbs
6
+ @breadcrumbs ||= Builder.new
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Hensel
2
+ VERSION = "0.0.1"
3
+ end
data/lib/hensel.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "hensel/version"
2
+ require "hensel/configuration"
3
+ require "hensel/builder"
4
+ require "hensel/helpers"
5
+
6
+ module Hensel
7
+ extend self
8
+
9
+ # Yields Hensel configuration block
10
+ # @example
11
+ # Hensel.configure do |config|
12
+ # config.attr_wrapper = '"'
13
+ # end
14
+ # @see Hensel::Configuration
15
+ def configure
16
+ yield configuration
17
+ configuration
18
+ end
19
+
20
+ # Returns Hensel configuration
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ # Resets Hensel configuration
26
+ def reset_configuration!
27
+ @configuration = nil
28
+ end
29
+ end
@@ -0,0 +1,202 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hensel::Builder do
4
+ let(:builder){ Hensel::Builder.new }
5
+
6
+ describe "#add" do
7
+ context "with basic usage" do
8
+ it "returns an instance of Builder::Item" do
9
+ expect(builder.add("Index", "/")).to be_an_instance_of(Hensel::Builder::Item)
10
+ end
11
+
12
+ it "adds an instance of Builder::Item to items" do
13
+ builder.add("Boom", "/boom")
14
+ expect(builder.items.last).to be_an_instance_of(Hensel::Builder::Item)
15
+ expect(builder.items.last.text).to eq("Boom")
16
+ end
17
+
18
+ it "allows to set options as the attribute of Builder::Item" do
19
+ builder.add("Foo", "/foo", class: "optional-class", id: "foo-id")
20
+ expect(builder.render).to have_tag(:li, class: "optional-class", id: "foo-id")
21
+ end
22
+ end
23
+
24
+ context "with optional usage" do
25
+ it "returns an instance of Builder::Item" do
26
+ expect(builder.add(text: "Index", url: "/")).to be_an_instance_of(Hensel::Builder::Item)
27
+ end
28
+
29
+ it "adds an instance of Builder::Item to items" do
30
+ builder.add(text: "Boom", url: "/boom")
31
+ expect(builder.items.last).to be_an_instance_of(Hensel::Builder::Item)
32
+ expect(builder.items.last.text).to eq("Boom")
33
+ end
34
+
35
+ it "allows to set options as the attribute of Builder::Item" do
36
+ builder.add(text: "Foo", url: "/foo", class: "optional-class", id: "foo-id")
37
+ expect(builder.render).to have_tag(:li, class: "optional-class", id: "foo-id")
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "#remove" do
43
+ context "with text" do
44
+ it "removes the item by text from items" do
45
+ tested = builder.add("Tested", "/tested")
46
+ builder.add("Sample", "/sample")
47
+ builder.remove("Tested")
48
+ expect(builder.items.any?{|x| x.text == tested.text }).to be_false
49
+ end
50
+ end
51
+
52
+ context "with block" do
53
+ it "removes the item by result of block from items" do
54
+ tested = builder.add("Tested", "/tested")
55
+ sample = builder.add("Sample", "/sample")
56
+ builder.remove{|x| x.url == "/sample" }
57
+ expect(builder.items.any?{|x| x.text == tested.text }).to be_true
58
+ expect(builder.items.any?{|x| x.text == sample.text }).to be_false
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "#render" do
64
+ context "with bootstrap" do
65
+ before { Hensel.configuration.bootstrap = true }
66
+ before(:each) do
67
+ builder.add("Index", "/", class: "dummy-class")
68
+ builder.add("Dummy", "/dummy", class: "dummy-class")
69
+ end
70
+ subject { builder.render }
71
+
72
+ it "respects the bootstrap style" do
73
+ expect(subject).to have_tag(:ul, with: { class: "breadcrumb" }) do
74
+ with_tag "li:last-child", class: "active"
75
+ end
76
+ end
77
+
78
+ it "respects the microdata rule" do
79
+ expect(subject).to have_tag(:ul, with: { class: "breadcrumb" }) do
80
+ with_tag :li, with: { itemtype: "http://data-vocabulary.org/Breadcrumb", itemscope: "itemscope" }
81
+ with_tag "li > a", with: { href: "/", itemprop: "url" }
82
+ with_tag "li > a > span", with: { itemprop: "title" }
83
+ end
84
+ end
85
+
86
+ after { Hensel.configuration.bootstrap = false }
87
+ end
88
+
89
+ context "with bootstrap and without richsnippet" do
90
+ before(:all) do
91
+ Hensel.configuration.richsnippet = nil
92
+ Hensel.configuration.bootstrap = true
93
+ end
94
+ before(:each) do
95
+ builder.add("Index", "/", class: "dummy-class")
96
+ builder.add("Dummy", "/dummy", class: "dummy-class")
97
+ end
98
+ subject { builder.render }
99
+
100
+ it "respects the bootstrap style" do
101
+ expect(subject).to have_tag(:ul, with: { class: "breadcrumb" }) do
102
+ with_tag "li:last-child", class: "active"
103
+ end
104
+ end
105
+
106
+ it "does not respect the microdata rule" do
107
+ expect(subject).not_to have_tag(:li, with: { itemtype: "http://data-vocabulary.org/Breadcrumb", itemscope: "itemscope" })
108
+ expect(subject).not_to have_tag("li > a", text: "Index", with: { href: "/", itemprop: "url" })
109
+ expect(subject).not_to have_tag("li > a", text: "Dummy", with: { href: "/dummy", itemprop: "url" })
110
+ end
111
+
112
+ after { Hensel.configuration.bootstrap = false }
113
+ end
114
+
115
+ context "without bootstrap" do
116
+ before(:each) do
117
+ builder.add("Index", "/", class: "dummy-class")
118
+ builder.add("Dummy", "/dummy", class: "dummy-class")
119
+ end
120
+ subject { builder.render }
121
+ it "does not respect the bootstrap style" do
122
+ expect(subject).not_to have_tag(:ul, with: { class: "breadcrumb" })
123
+ expect(subject).not_to have_tag("li:last-child", with: { class: "active" })
124
+ end
125
+ end
126
+
127
+ context "attr_wrapper" do
128
+ before { Hensel.configuration.attr_wrapper = '"' }
129
+ before(:each){ builder.add("Index", "/", class: "dummy-class") }
130
+ subject { builder.render }
131
+ it { should_not match(/'/) }
132
+ it { should match(/"/) }
133
+ after { Hensel.reset_configuration! }
134
+ end
135
+
136
+ context "whitespace" do
137
+ before do
138
+ Hensel.configuration.richsnippet = nil
139
+ Hensel.configuration.whitespace = " "
140
+ end
141
+ before(:each){ builder.add("Index", "/", class: "dummy-class") }
142
+ let(:fixture){
143
+ <<-FIXTURE.chomp
144
+ <ul>
145
+ <li class='dummy-class'>
146
+ <span>
147
+ Index
148
+ </span>
149
+ </li>
150
+ </ul>
151
+ FIXTURE
152
+ }
153
+ subject { builder.render }
154
+ it { should eq(fixture) }
155
+ end
156
+ end
157
+
158
+ describe "customized breadcrumbs" do
159
+ context "use customizable renderer instead of default renderer" do
160
+ before do
161
+ builder.add("Index", "/")
162
+ builder.add("Dummy", "/dummy")
163
+ end
164
+ subject do
165
+ builder.render do
166
+ node(:custom, href: item.url){ item.text }
167
+ end
168
+ end
169
+
170
+ it "can create the html of breadcrumb correctly" do
171
+ builder.add("Index", "/")
172
+ actual_html = builder.render do
173
+ node(:span){ "text: #{item.text}, url: #{item.url}" }
174
+ end
175
+ expect(subject).to have_tag(:ul) do
176
+ with_tag :custom, href: "/"
177
+ with_tag :custom, href: "/dummy"
178
+ end
179
+ end
180
+ end
181
+
182
+ context "parent element is not ul but div" do
183
+ before do
184
+ Hensel.configuration.parent_element = :div
185
+ Hensel.configuration.bootstrap = false
186
+ builder.add("Index", "/")
187
+ end
188
+ subject { builder.render }
189
+
190
+ it "can set parent element for customizable breadcrumb" do
191
+ expect(subject).to have_tag(:div, { class: "breadcrumb" }) do
192
+ with_tag "li", class: "active"
193
+ end
194
+ end
195
+
196
+ after do
197
+ Hensel.configuration.parent_element = :ul
198
+ Hensel.configuration.bootstrap = false
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hensel::Configuration do
4
+ describe ".attr_boolean_accessor" do
5
+ let(:configuration){ Hensel::Configuration.new }
6
+ it "can define an accessor and boolean methods" do
7
+ expect(configuration.respond_to?(:sample)).to be_false
8
+ Hensel::Configuration.attr_boolean_accessor :sample
9
+ expect(configuration.respond_to?(:sample)).to be_true
10
+ end
11
+ end
12
+
13
+ describe "#[]" do
14
+ end
15
+
16
+ describe "#[]=" do
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hensel::Helpers do
4
+ let(:object){ Object.new }
5
+ before { object.extend Hensel::Helpers }
6
+ subject{ object.breadcrumbs }
7
+ it { should be_an_instance_of(Hensel::Builder) }
8
+ end
data/spec/item_spec.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hensel::Builder::Item do
4
+ let(:builder){ Hensel::Builder.new }
5
+
6
+ context "with escape_html" do
7
+ before(:all) do
8
+ Hensel.configuration.escape_html = true
9
+ Hensel.configuration.indentation = false
10
+ end
11
+ before(:each){ builder.add('\'&"<>', '/') }
12
+ subject { builder.items.last.text }
13
+ it { should eq('&#39;&amp;&quot;&lt;&gt;') }
14
+ end
15
+
16
+ context "without escape_html" do
17
+ before(:all) do
18
+ Hensel.configuration.escape_html = false
19
+ Hensel.configuration.indentation = false
20
+ end
21
+ before(:each){ builder.add('\'&"<>', '/') }
22
+ subject { builder.items.last.text }
23
+ it { should eq('\'&"<>') }
24
+ end
25
+
26
+ describe "#render" do
27
+ let(:item) { Hensel::Builder::Item.new("index", "/") }
28
+ subject { item.render }
29
+ context "basic usage" do
30
+ it { should have_tag(:li){ with_tag(:a, href: "/") } }
31
+ end
32
+
33
+ context "with customized renderer" do
34
+ it "can be set to renderer" do
35
+ item.renderer = ->(this){ node(:custom, data: "sample"){ item.text } }
36
+ should have_tag(:custom, text: "index", data: "sample")
37
+ end
38
+ end
39
+ end
40
+ end
data/spec/node_spec.rb ADDED
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hensel::Builder::Node do
4
+ let(:item) { Hensel::Builder::Item.new("index", "/") }
5
+ before { Hensel.configuration.indentation = false }
6
+
7
+ describe "#render" do
8
+ end
9
+
10
+ describe "#node" do
11
+ context "with block" do
12
+ subject { item.render }
13
+ it "can build html correctly" do
14
+ item.renderer = ->(this){ node(:div, class: "hey"){ "sample text" }}
15
+ should have_tag(:div, text: "sample text", class: "hey")
16
+ end
17
+
18
+ it "can build html correctly even when node is nested" do
19
+ item.renderer = ->(this){ node(:div, class: "hey"){ node(:span){ "nested text" } }}
20
+ should have_tag(:div, class: "hey") do
21
+ with_tag(:span, text: "nested text")
22
+ end
23
+ end
24
+
25
+ it "should support same hierarchy elements" do
26
+ item.renderer = ->(this){
27
+ node(:div) do
28
+ node(:span){ "one1" }
29
+ node(:span){ "one2" }
30
+ end
31
+ }
32
+ should have_tag(:div) do
33
+ with_tag(:span, text: "one1")
34
+ with_tag(:span, text: "one2")
35
+ end
36
+ end
37
+ end
38
+
39
+ context "without block" do
40
+ subject { item.render }
41
+ it "can build html correctly" do
42
+ item.renderer = ->(this){ node(:div, "sample text", class: "hey") }
43
+ should have_tag(:div, content: "sample text", class: "hey")
44
+ end
45
+
46
+ it "can build html correctly even when node is nested" do
47
+ item.renderer = ->(this){ node(:div, class: "hey"){ node(:span, "nested text") }}
48
+ should have_tag(:div, class: "hey") do
49
+ with_tag(:span, content: "nested text")
50
+ end
51
+ end
52
+
53
+ it "should support same hierarchy elements" do
54
+ item.renderer = ->(this){
55
+ node(:div) do
56
+ node(:span, "one1")
57
+ node(:span, "one2")
58
+ end
59
+ }
60
+ should have_tag(:div) do
61
+ with_tag(:span, text: "one1")
62
+ with_tag(:span, text: "one2")
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#item" do
69
+ subject { item.render }
70
+ it "can be referred from block" do
71
+ item.renderer = ->(this){ node(:div, item.text) }
72
+ should have_tag(:div, text: "index")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,2 @@
1
+ require File.expand_path('../../lib/hensel', __FILE__)
2
+ require 'rspec-html-matchers'
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hensel::Helpers::TagHelpers do
4
+ let(:helpers){ Object.new }
5
+ before { helpers.extend Hensel::Helpers::TagHelpers }
6
+
7
+ describe "#contegt_tag" do
8
+ context "with block" do
9
+ subject { helpers.content_tag(:div, class: "dummy-class"){ "hello" }}
10
+ it { should have_tag(:div, content: "hello", class: "dummy-class") }
11
+ end
12
+
13
+ context "without block" do
14
+ subject { helpers.content_tag(:div, "hello", class: "dummy-class")}
15
+ it { should have_tag(:div, content: "hello", class: "dummy-class") }
16
+ end
17
+
18
+ context "with indentation" do
19
+ before { Hensel.configuration.indentation = true }
20
+ subject { helpers.content_tag(:div, "hello", indent: 2, class: "dummy-class")}
21
+ it { expect(subject.start_with?(" ")).to be_true }
22
+ end
23
+
24
+ context "without indentation" do
25
+ before { Hensel.configuration.indentation = false }
26
+ subject { helpers.content_tag(:div, "hello", indent: 2, class: "dummy-class")}
27
+ it { expect(subject.start_with?("<div")).to be_true }
28
+ end
29
+ end
30
+
31
+ describe "#tag" do
32
+ subject { helpers.tag(:img, class: "sample-image", src: "sample.jpg", alt: "sample image") }
33
+ it { should have_tag(:img, class: "sample-image", src: "sample.jpg", alt: "sample image") }
34
+ end
35
+
36
+ describe "#append_attribute" do
37
+ subject { Hash.new }
38
+ before { helpers.append_attribute(:key, :value, subject) }
39
+ it { subject[:key].should eq(:value) }
40
+ end
41
+ end