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