excalibur 0.0.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3a2f5e3f336d2c2b9cd05b21f50387fca47779dd
4
+ data.tar.gz: ef5f966e21a9f27b7c23db57533d5c6f6dece688
5
+ SHA512:
6
+ metadata.gz: 51506887560a0d4771933ef36713ca814c96ed4c2c12fc2ba31cdafd214a7b982c15d0df47a9d33c70bef15e5f03bc50bd5f6086bc55aa1439b5b3a803eda9c3
7
+ data.tar.gz: eefbce53136c980ee7be535160ed801efc794052bb84686bfcde9152bc35319af85034fc059b19cba6ad9f2383db0d657a28cea935eea4ea6a0896cd62c3623a
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in excalibur.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Joost (yopefonic) Elfering
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # Excalibur
2
+
3
+ Excalibur is a SEO gem for [Ruby on Rails](rubyonrails.org) and helps you to
4
+ set the title and meta tags for you site overall and per page. Unlike other
5
+ options like [meta-tags](https://github.com/kpumuk/meta-tags) and
6
+ [meta_magic](https://github.com/lassebunk/metamagic) Excalibur focusses on
7
+ providing an object based DSL to turn the objects you are presenting on the
8
+ page into SEO related tags in the head.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'excalibur'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install excalibur
23
+
24
+ ### Initialize
25
+
26
+ To add an initializer to your application execute:
27
+
28
+ rails generate excalibur:install
29
+
30
+ The initializer is documented with the possible options you have to customize
31
+ your default setup of Excalibur.
32
+
33
+ ## Usage
34
+
35
+ ### Layout
36
+
37
+ In your layout you can replace your title tag with the Excalibur render title
38
+ method:
39
+
40
+ ```erb
41
+ <%= render_title_tag %>
42
+ ```
43
+
44
+ To render meta tags place this between the ```<head>``` tags of your layout:
45
+
46
+ ```erb
47
+ <%= render_meta_tags %>
48
+ ```
49
+
50
+ ### Views
51
+
52
+ As excalibur believes in separation of concerns passing the current object you
53
+ want to render title and meta tags for is done in the view.
54
+
55
+ ```erb
56
+ <% entitle @your_object %>
57
+ ```
58
+
59
+ The object your passing into the entitle method will search for a Excalibur
60
+ decorator. You can find out more on how to create them in the
61
+ [decorators](#decorators) section.
62
+
63
+ To modify the configuration in the view you can pass an option into the
64
+ ```entitle``` method and pass it a new excalibur config object. Read more
65
+ about creating your own in the [configurations](#configurations) section. It
66
+ will try and merge the supplied configuration with the one specified in the
67
+ decorator. Because of this you need to supply it with the same type of object
68
+ to make that possible.
69
+
70
+ Notably the second argument of ```entitle``` can also pass other
71
+ [Draper](https://github.com/drapergem/draper) related options like draper's
72
+ context.
73
+
74
+ ```ruby
75
+ entitle @your_object, config: @custom_config
76
+ ```
77
+
78
+ ### Decorators
79
+
80
+ The decorators are what turns your object into data that can be used for the
81
+ meta tags and the title. So without a decorator for an object the view helper
82
+ ``` entitle ``` will have no use at all.
83
+
84
+ To scaffold a decorator execute:
85
+
86
+ rails generate excalibur:install [class name]
87
+
88
+ A detailed description on how the decorators work can be found in the newly
89
+ created decorator class. As a side note the ```Excalibur::Decorator``` is a
90
+ subclass of ```Draper::Decorator``` and comes with the free functionality
91
+ provided by [Draper](https://github.com/drapergem/draper). Have fun!
92
+
93
+ ### Configurations
94
+
95
+ **WARNING:** handle with care!
96
+
97
+ So you want to roll your own configuration, awesome!
98
+ ```Excalibur::Configuration``` can be initialized with 3 arguments that are
99
+ the title, description and meta tags.
100
+
101
+ Both title and description are an instance of
102
+ ```Excalibur::TruncateableContent``` but as long as you make sure a
103
+ ```.to_s``` with one argument is present for the object you put in there it is
104
+ fine. The object passed to it is either a decorated object when it is set or a
105
+ blank decorated object.
106
+
107
+ ```Excalibur::TruncateableContent``` can be of course used on it's own when
108
+ creating a new configuration. It also takes 3 arguments. Two hashes; the first
109
+ for the content and the second for options. On it's own the
110
+ TruncateableContent does not do a lot but the third argument takes a
111
+ ```Proc``` that is called with the decorator on render. By default the proc
112
+ combines the content according to the content and the options supplied in the
113
+ first two hashes. But of course you can supply your own. Setting them to empty
114
+ hashes or nil will work and when merged the default will keep in place.
115
+
116
+ The meta tags, and third argument of the configuration is a structured hash
117
+ with two layers. The first layer is the global type of meta tag and the second
118
+ contains the sub-types with the content.
119
+
120
+ so a meta tag like this:
121
+
122
+ ```html
123
+ <meta name="description" content="Excalibur is cool" />
124
+ ```
125
+
126
+ you need a hash like this:
127
+
128
+ ```ruby
129
+ {
130
+ name: {
131
+ description: 'Excalibur is cool'
132
+ }
133
+ }
134
+ ```
135
+
136
+ For an OpenGraph tag this would be something like:
137
+
138
+ ```html
139
+ <meta property="og:description" content="Excalibur is cool" />
140
+ ```
141
+
142
+ ```ruby
143
+ {
144
+ property: {
145
+ 'og:description' => 'Excalibur is cool'
146
+ }
147
+ }
148
+ ```
149
+
150
+ The content of the meta_tags can be defined as an Array of items or just a
151
+ single one. The item itself can be any type of variable that render as a
152
+ string on a page. It can also be a ```Proc``` that will be passed the
153
+ decorated object just like the title and description. This Proc can also
154
+ result in an array.
155
+
156
+ ## Contributing
157
+
158
+ 1. Fork it ( https://github.com/yopefonic/excalibur/fork )
159
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
160
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
161
+ 4. Push to the branch (`git push origin my-new-feature`)
162
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/excalibur.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'excalibur/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'excalibur'
8
+ spec.version = Excalibur::VERSION
9
+ spec.authors = ['Joost Elfering']
10
+ spec.email = ['yopefonic@gmail.com']
11
+ spec.summary = %q{helper gem to set page title and meta tags in a rails app}
12
+ spec.description = %q{helper gem for rails apps to set the title and meta tags for a page. The helper can take custom input per page, objects that use decorator to determine the tags or an application wide configurable default.}
13
+ spec.homepage = 'https://github.com/yopefonic/excalibur'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.required_ruby_version = '>= 1.9.3'
22
+
23
+ spec.add_dependency 'rails', '~> 3'
24
+ spec.add_dependency 'draper', '~> 1.3', '>= 1.3.0'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.6'
27
+ spec.add_development_dependency 'rake', '~> 10.3'
28
+ spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_development_dependency 'generator_spec', '~> 0.9', '>= 0.9.2'
30
+ spec.add_development_dependency 'simplecov', '~> 0.8', '>= 0.8.0'
31
+ spec.add_development_dependency 'pry', '~> 0.10', '>= 0.10.1'
32
+ end
@@ -0,0 +1,77 @@
1
+ module Excalibur
2
+ # the Configuration class is responsible for holding the configurable data
3
+ # and logic that will be passed down
4
+ class Configuration
5
+ include Duplicator
6
+
7
+ attr_accessor :title
8
+ attr_accessor :description
9
+ attr_accessor :meta_tags
10
+
11
+ def initialize(
12
+ title = TruncateableContent.new,
13
+ description = TruncateableContent.new,
14
+ meta_tags = ::HashWithIndifferentAccess.new({}))
15
+ @title = title
16
+ @description = description
17
+ @meta_tags = meta_tags
18
+ end
19
+
20
+ def merge!(obj)
21
+ if obj.is_a? Configuration
22
+ @title = merge_instance(@title, obj.title)
23
+ @description = merge_instance(@description, obj.description)
24
+ @meta_tags = merge_instance(@meta_tags, obj.meta_tags)
25
+
26
+ self
27
+ else
28
+ fail(TypeError.new(true),
29
+ 'can only merge two Excalibur::Configuration objects')
30
+ end
31
+ end
32
+
33
+ def dup
34
+ self.class.new(
35
+ dup_instance(@title),
36
+ dup_instance(@description.dup),
37
+ dup_instance(@meta_tags.dup))
38
+ end
39
+
40
+ def set_meta_tag(type, name, value = nil)
41
+ if @meta_tags[type].nil?
42
+ @meta_tags[type] = ::HashWithIndifferentAccess.new
43
+ end
44
+
45
+ @meta_tags[type][name] = value
46
+ end
47
+
48
+ def remove_meta_tag(type, name)
49
+ @meta_tags[type].delete(name) if @meta_tags[type].present?
50
+ @meta_tags.delete(type) if @meta_tags[type].empty?
51
+ end
52
+
53
+ private
54
+
55
+ def merge_instance(old, new)
56
+ if old.is_a? new.class
57
+ merge_content(old, new)
58
+ elsif !new.nil?
59
+ new
60
+ else
61
+ old
62
+ end
63
+ end
64
+
65
+ def merge_content(old, new)
66
+ if old.is_a? ::Hash
67
+ old.deep_merge!(new)
68
+ elsif old.is_a? TruncateableContent
69
+ old.merge!(new)
70
+ elsif old.is_a? ::String
71
+ old + new
72
+ else
73
+ new
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,55 @@
1
+ require 'draper'
2
+
3
+ module Excalibur
4
+ # the Decorator class helps content to be derived from application base
5
+ # object and turn them into Excalibur titles and meta tags. It's main
6
+ # responsibilities are to make local classes configurable and it acts as a
7
+ # connector between the application's objects and Excalibur.
8
+ class Decorator < ::Draper::Decorator
9
+ delegate_all
10
+
11
+ attr_accessor :custom_configuration
12
+
13
+ class << self
14
+ attr_writer :configuration
15
+
16
+ def excalibur_init(config = configuration)
17
+ @configuration = config
18
+ end
19
+
20
+ def configuration
21
+ @configuration ||= ::Excalibur.configuration.dup
22
+ end
23
+
24
+ def method_missing(meth, *args)
25
+ if meth.to_s =~ /^excalibur_set_(title|description+)_(content|option|combinator+)$/
26
+ configuration.send($1).send("update_#{$2}", *args)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def excalibur_set_meta_tag(type, name, value = nil)
33
+ configuration.set_meta_tag(type, name, value)
34
+ end
35
+ end
36
+
37
+ def initialize(object, options = {})
38
+ configuration.merge!(options.delete(:config)) if options.key?(:config)
39
+
40
+ super(object, options)
41
+ end
42
+
43
+ def configuration
44
+ @custom_configuration ||= self.class.configuration.dup
45
+ end
46
+
47
+ def customize_configuration(config)
48
+ configuration.merge!(config)
49
+ end
50
+
51
+ def render_title(obj = self)
52
+ configuration.title.to_s(obj) if configuration.title.present?
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,19 @@
1
+ module Excalibur
2
+ # the Duplicator module helps in making sure duplication of nested objects
3
+ # functions properly and is used by the class Configuration and the class
4
+ # TruncableContent
5
+ module Duplicator
6
+ # duplicates TruncatableContent and calls the deep_dup method on any form
7
+ # of Hash. Otherwise return the object as not all objects do not need to
8
+ # be duplicated.
9
+ def dup_instance(obj)
10
+ if obj.is_a?(TruncateableContent)
11
+ obj.dup
12
+ elsif obj.is_a?(Hash)
13
+ obj.deep_dup
14
+ else
15
+ obj
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ require 'excalibur/view_helpers'
2
+
3
+ module Excalibur
4
+ # ties the ViewHelpers into Rails and allows them to be used in the views
5
+ class Railtie < Rails::Railtie
6
+ initializer 'excalibur.view_helpers' do
7
+ ActionView::Base.send :include, ViewHelpers
8
+ end
9
+
10
+ generators do
11
+ require 'generators/excalibur/install_generator'
12
+ require 'generators/excalibur/decorator_generator'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,82 @@
1
+ module Excalibur
2
+ # The TruncateableContent class is responsible for text content that is
3
+ # constrained in length due to SEO specifications. Mainly used for title
4
+ # and meta description tags this class facilitates the creation and proper
5
+ # rendering of the content. A prefix/body/suffix data structure is used so
6
+ # that only the body is truncated without having an effect on the branding
7
+ # that is put before and/or after the body.
8
+ class TruncateableContent
9
+ include Duplicator
10
+
11
+ attr_accessor :content
12
+ attr_accessor :options
13
+ attr_accessor :combinator
14
+
15
+ def initialize(content = {}, options = {}, combinator = nil)
16
+ @content = ::HashWithIndifferentAccess.new(content)
17
+ @options = ::HashWithIndifferentAccess.new(options)
18
+ @combinator = combinator
19
+ end
20
+
21
+ def can_merge?(obj)
22
+ obj.is_a? TruncateableContent
23
+ end
24
+
25
+ def merge!(obj)
26
+ if can_merge?(obj)
27
+ @content.merge!(obj.content)
28
+ @options.merge!(obj.options)
29
+ @combinator = obj.combinator unless obj.combinator.nil?
30
+
31
+ self
32
+ else
33
+ fail(TypeError.new(true),
34
+ 'can only merge two Excalibur::TruncateableContent objects')
35
+ end
36
+ end
37
+
38
+ def dup
39
+ self.class.new(
40
+ dup_instance(@content),
41
+ dup_instance(@options),
42
+ dup_instance(@combinator)
43
+ )
44
+ end
45
+
46
+ def get_content(key, obj = nil)
47
+ if @content[key].instance_of?(Proc)
48
+ @content[key].call(obj)
49
+ elsif @content[key].nil?
50
+ ''
51
+ else
52
+ @content[key]
53
+ end
54
+ end
55
+
56
+ def update_content(key, value = nil)
57
+ @content[key] = value
58
+ end
59
+
60
+ def update_option(key, value = nil)
61
+ @options[key] = value
62
+ end
63
+
64
+ def update_combinator(value = nil)
65
+ @combinator = value
66
+ end
67
+
68
+ def render_long(obj = nil)
69
+ @content.map { |key, _value| get_content(key, obj).to_s }.inject(:+).to_s
70
+ end
71
+
72
+ def render_short(obj = nil)
73
+ if @combinator.instance_of? Proc
74
+ @combinator.call(obj)
75
+ else
76
+ render_long(obj)
77
+ end
78
+ end
79
+
80
+ alias_method :to_s, :render_short
81
+ end
82
+ end
@@ -0,0 +1,4 @@
1
+ # setting version number for the Excalibur gem
2
+ module Excalibur
3
+ VERSION = '0.0.2'
4
+ end
@@ -0,0 +1,59 @@
1
+ module Excalibur
2
+ # the ViewHelpers module contains the methods to access Excalibur from the
3
+ # view. Most commonly placed in the view templates and the application Layout
4
+ module ViewHelpers
5
+ def entitle(object, options = {})
6
+ @excalibur_subject = excalibur_decorate_subject(object, options)
7
+ end
8
+
9
+ def render_title_tag
10
+ content_tag :title, excalibur_subject.render_title
11
+ end
12
+
13
+ def render_meta_tags
14
+ result = ''
15
+
16
+ excalibur_subject.configuration.meta_tags.each do |type_name, value|
17
+ value.each do |type_value, contents|
18
+ next if contents.nil?
19
+ contents = render_meta_content(contents)
20
+ contents = [contents] unless contents.is_a?(Array)
21
+
22
+ contents.each do |content|
23
+ result << tag(
24
+ :meta,
25
+ type_name => type_value,
26
+ content: content
27
+ )
28
+ end
29
+ end
30
+ end
31
+
32
+ result.html_safe
33
+ end
34
+
35
+ private
36
+
37
+ def render_meta_content(content)
38
+ if content.is_a? Proc
39
+ content.call(excalibur_subject)
40
+ else
41
+ content
42
+ end
43
+ end
44
+
45
+ def excalibur_subject
46
+ @excalibur_subject ||= new_blank_excalibur_subject
47
+ end
48
+
49
+ def new_blank_excalibur_subject
50
+ ::Excalibur::Decorator.decorate(true)
51
+ end
52
+
53
+ def excalibur_decorate_subject(object, options = {})
54
+ Object.const_get(
55
+ "::Excalibur::#{object.class.name}Decorator"
56
+ ).decorate(object, options)
57
+ end
58
+ end
59
+ end
data/lib/excalibur.rb ADDED
@@ -0,0 +1,91 @@
1
+ require 'excalibur/version'
2
+ require 'active_support/all'
3
+ require 'excalibur/decorator'
4
+ require 'excalibur/duplicator'
5
+ require 'excalibur/configuration'
6
+ require 'excalibur/truncatable_content'
7
+ require 'excalibur/railtie' if defined?(Rails)
8
+
9
+ # the Excalibur gem helps you to decorate a rails app with proper title and
10
+ # meta tags for every page. It does this by providing default configuration
11
+ # options, object based decorators for customization per object type and
12
+ # helpers to put in on the page.
13
+ module Excalibur
14
+ class << self
15
+ attr_writer :configuration
16
+
17
+ def configuration
18
+ @configuration ||= new_default_configuration
19
+ end
20
+
21
+ def reset
22
+ @configuration = new_default_configuration
23
+ end
24
+
25
+ # defines the gem default
26
+ def new_default_configuration
27
+ Configuration.new(
28
+ new_default_title,
29
+ new_default_description,
30
+ new_default_meta_tags
31
+ )
32
+ end
33
+
34
+ def configure
35
+ yield(configuration)
36
+ end
37
+
38
+ private
39
+
40
+ def new_default_title
41
+ TruncateableContent.new(
42
+ { body: 'Excalibur' },
43
+ { length: 69, omission: '...', separator: '' },
44
+ proc do |obj|
45
+ if obj.present?
46
+ t = obj.configuration.title
47
+
48
+ length = t.options[:length]
49
+ length -= t.get_content(:prefix, obj).length
50
+ length -= t.get_content(:suffix, obj).length
51
+
52
+ res = t.get_content(:prefix, obj)
53
+ res += t.get_content(:body, obj).truncate(length, t.options)
54
+ res + t.get_content(:suffix, obj)
55
+ end
56
+ end
57
+ )
58
+ end
59
+
60
+ def new_default_description
61
+ TruncateableContent.new(
62
+ { body: 'Excalibur; a worthy title for a gem about titles.' },
63
+ { length: 155, omission: '...', separator: ' ' },
64
+ proc do |obj|
65
+ if obj.present?
66
+ d = obj.configuration.description
67
+
68
+ length = d.options[:length]
69
+ length -= d.get_content(:prefix, obj).length
70
+ length -= d.get_content(:suffix, obj).length
71
+
72
+ res = d.get_content(:prefix, obj)
73
+ res += d.get_content(:body, obj).truncate(length, d.options)
74
+ res + d.get_content(:suffix, obj)
75
+ end
76
+ end
77
+ )
78
+ end
79
+
80
+ def new_default_meta_tags
81
+ ::HashWithIndifferentAccess.new(
82
+ name: ::HashWithIndifferentAccess.new(
83
+ description: proc do |obj|
84
+ obj.configuration.description.to_s(obj)
85
+ end,
86
+ viewport: 'width=device-width, initial-scale=1'
87
+ )
88
+ )
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Excalibur
4
+ module Generators
5
+ # allows you to use rails generate to create a new scaffolded decorator
6
+ # for a application native class.
7
+ # rails g excalibur:decorator [class_name]
8
+ class DecoratorGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('../../templates', __FILE__)
10
+
11
+ desc 'Creates a Excalibur decorator.'
12
+
13
+ def create_decorator
14
+ template 'decorator.rb', "app/decorators/excalibur/#{file_name}_decorator.rb"
15
+ end
16
+ end
17
+ end
18
+ end