mbrao 1.4.4 → 1.5.0

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -0
  3. data/.travis.yml +3 -4
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +4 -4
  6. data/doc/ActionView.html +125 -0
  7. data/doc/ActionView/Template.html +140 -0
  8. data/doc/ActionView/Template/Handlers.html +3 -3
  9. data/doc/ActionView/Template/Handlers/MbraoTemplate.html +26 -26
  10. data/doc/HTML.html +2 -2
  11. data/doc/HTML/Pipeline.html +2 -2
  12. data/doc/HTML/Pipeline/KramdownFilter.html +4 -4
  13. data/doc/Mbrao.html +3 -3
  14. data/doc/Mbrao/Author.html +29 -29
  15. data/doc/Mbrao/Content.html +1977 -3644
  16. data/doc/Mbrao/ContentInterface.html +817 -0
  17. data/doc/Mbrao/ContentInterface/ClassMethods.html +388 -0
  18. data/doc/Mbrao/Exceptions.html +1 -1
  19. data/doc/Mbrao/Exceptions/InvalidDate.html +1 -1
  20. data/doc/Mbrao/Exceptions/InvalidMetadata.html +1 -1
  21. data/doc/Mbrao/Exceptions/Parsing.html +1 -1
  22. data/doc/Mbrao/Exceptions/Rendering.html +1 -1
  23. data/doc/Mbrao/Exceptions/UnavailableLocalization.html +1 -1
  24. data/doc/Mbrao/Exceptions/Unimplemented.html +1 -1
  25. data/doc/Mbrao/Exceptions/UnknownEngine.html +1 -1
  26. data/doc/Mbrao/Parser.html +12 -12
  27. data/doc/Mbrao/ParserInterface.html +134 -0
  28. data/doc/Mbrao/ParserInterface/ClassMethods.html +1724 -0
  29. data/doc/Mbrao/ParserValidations.html +134 -0
  30. data/doc/Mbrao/ParserValidations/ClassMethods.html +348 -0
  31. data/doc/Mbrao/ParsingEngines.html +1 -1
  32. data/doc/Mbrao/ParsingEngines/Base.html +4 -4
  33. data/doc/Mbrao/ParsingEngines/PlainText.html +25 -15
  34. data/doc/Mbrao/RenderingEngines.html +1 -1
  35. data/doc/Mbrao/RenderingEngines/Base.html +2 -2
  36. data/doc/Mbrao/RenderingEngines/HtmlPipeline.html +173 -169
  37. data/doc/Mbrao/Version.html +3 -3
  38. data/doc/_index.html +51 -24
  39. data/doc/class_list.html +1 -1
  40. data/doc/file.README.html +1 -1
  41. data/doc/index.html +1 -1
  42. data/doc/method_list.html +63 -69
  43. data/doc/top-level-namespace.html +2 -2
  44. data/lib/mbrao.rb +7 -2
  45. data/lib/mbrao/author.rb +5 -5
  46. data/lib/mbrao/content.rb +110 -256
  47. data/lib/mbrao/content_interface.rb +146 -0
  48. data/lib/mbrao/exceptions.rb +1 -1
  49. data/lib/mbrao/integrations/rails.rb +48 -42
  50. data/lib/mbrao/parser.rb +24 -176
  51. data/lib/mbrao/parser_interface.rb +143 -0
  52. data/lib/mbrao/parser_validations.rb +41 -0
  53. data/lib/mbrao/parsing_engines/base.rb +4 -4
  54. data/lib/mbrao/parsing_engines/plain_text.rb +136 -121
  55. data/lib/mbrao/rendering_engines/base.rb +2 -2
  56. data/lib/mbrao/rendering_engines/html_pipeline.rb +52 -77
  57. data/lib/mbrao/rendering_engines/html_pipeline/kramdown_filter.rb +31 -0
  58. data/lib/mbrao/version.rb +2 -2
  59. data/mbrao.gemspec +3 -3
  60. data/spec/mbrao/author_spec.rb +1 -1
  61. data/spec/mbrao/content_spec.rb +1 -1
  62. data/spec/mbrao/parser_spec.rb +16 -16
  63. data/spec/mbrao/rendering_engines/html_pipeline/kramdown_filter_spec.rb +28 -0
  64. data/spec/mbrao/rendering_engines/html_pipeline_spec.rb +0 -21
  65. metadata +23 -8
@@ -0,0 +1,146 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the mbrao gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module Mbrao
8
+ # Miscellaneous {Content Content} class methods.
9
+ module ContentInterface
10
+ extend ActiveSupport::Concern
11
+
12
+ # The allowed string format for a datetime.
13
+ ALLOWED_DATETIME_FORMATS = [
14
+ "%Y%m%dT%H%M%S%z", "%Y%m%dT%H%M%S%Z",
15
+ "%FT%T.%L%z", "%FT%T.%L%Z",
16
+ "%FT%T%z", "%FT%T%Z",
17
+ "%F %T %z", "%F %T %Z",
18
+ "%F %T.%L %z", "%F %T.%L %Z",
19
+
20
+ "%F %T.%L", "%F %T", "%F %H:%M", "%F",
21
+ "%d/%m/%Y %T.%L", "%d/%m/%Y %T", "%d/%m/%Y %H:%M", "%d/%m/%Y"
22
+ ]
23
+
24
+ # Class methods.
25
+ module ClassMethods
26
+ # Validates locales for attribute retrieval.
27
+ #
28
+ # @param locales [Array] A list of desired locales for an attribute. Can include `*` to match all. If none are specified, the default mbrao locale will be
29
+ # used.
30
+ # @param content [Content|nil] An optional content to check for availability
31
+ # @return [Array] The validated list of locales.
32
+ def validate_locales(locales, content = nil)
33
+ locales = locales.ensure_array(nil, true, true, true) { |l| l.ensure_string.strip }
34
+ locales = (locales.empty? ? [Mbrao::Parser.locale] : locales)
35
+ raise Mbrao::Exceptions::UnavailableLocalization if content && !content.enabled_for_locales?(locales)
36
+ locales
37
+ end
38
+
39
+ # Creates a content with metadata and body.
40
+ #
41
+ # @param metadata [Hash] The metadata.
42
+ # @param body [String] The body of the content.
43
+ # @return [Content] A new content.
44
+ def create(metadata, body)
45
+ rv = Mbrao::Content.new
46
+ rv.body = body.ensure_string.strip
47
+ assign_metadata(rv, metadata.symbolize_keys) if metadata.is_a?(Hash)
48
+ rv
49
+ end
50
+ end
51
+
52
+ # Checks if the content is available for at least one of the provided locales.
53
+ #
54
+ # @param locales [Array] The desired locales. Can include `*` to match all. If none are specified, the default mbrao locale will be used.
55
+ # @return [Boolean] `true` if the content is available for at least one of the desired locales, `false` otherwise.
56
+ def enabled_for_locales?(*locales)
57
+ locales = locales.flatten.ensure_array(nil, false, false, true) { |l| l.ensure_string.strip }.reject { |l| l == "*" }
58
+ @locales.blank? || locales.blank? || (@locales & locales).present?
59
+ end
60
+
61
+ # Gets the title of the content in the desired locales.
62
+ #
63
+ # @param locales [String|Array] The desired locales. Can include `*` to match all. If none are specified, the default mbrao locale will be used.
64
+ # @return [String|HashWithIndifferentAccess] Return the title of the content in the desired locales. If only one locale is required, then a `String`
65
+ # is returned, else a `HashWithIndifferentAccess` with locales as keys.
66
+ def get_title(locales = [])
67
+ filter_attribute_for_locales(@title, locales)
68
+ end
69
+
70
+ # Gets the body returning only the portion which are available for the given locales.
71
+ #
72
+ # @param locales [String|Array] The desired locales. Can include `*` to match all. If none are specified, the default mbrao locale will be used.
73
+ # @param engine [String|Symbol|Object] The engine to use to filter contents.
74
+ # @return [String|HashWithIndifferentAccess] Return the body of the content in the desired locales. If only one locale is required, then a `String`
75
+ # is returned, else a `HashWithIndifferentAccess` with locales as keys.
76
+ def get_body(locales = [], engine = :plain_text)
77
+ Mbrao::Parser.create_engine(engine).filter_content(self, locales)
78
+ end
79
+
80
+ # Gets the tags of the content in the desired locales.
81
+ #
82
+ # @param locales [String|Array] The desired locales. Can include `*` to match all. If none are specified, the default mbrao locale will be used.
83
+ # @return [Array|HashWithIndifferentAccess] Return the title of the content in the desired locales. If only one locale is required, then a `Array`
84
+ # is returned, else a `HashWithIndifferentAccess` with locales as keys.
85
+ def get_tags(locales = [])
86
+ filter_attribute_for_locales(@tags, locales)
87
+ end
88
+
89
+ # Gets the "more link" text of the content in the desired locales.
90
+ #
91
+ # @param locales [String|Array] The desired locales. Can include `*` to match all. If none are specified, the default mbrao locale will be used.
92
+ # @return [String|HashWithIndifferentAccess] Return the label of the "more link" of the content in the desired locales. If only one locale is required,
93
+ # then a `String` is returned, else a `HashWithIndifferentAccess` with locales as keys.
94
+ def get_more(locales = [])
95
+ filter_attribute_for_locales(@more, locales)
96
+ end
97
+
98
+ # Returns the content as an Hash.
99
+ #
100
+ # @param options [Hash] Options to modify behavior of the serialization.
101
+ # The only supported value are:
102
+ #
103
+ # * `:exclude`, an array of attributes to skip.
104
+ # * `:exclude_empty`, if to exclude nil values. Default is `false`.
105
+ # @return [Hash] An hash with all attributes.
106
+ def as_json(options = {})
107
+ keys = [:uid, :locales, :title, :summary, :body, :tags, :more, :author, :created_at, :updated_at, :metadata]
108
+ ::Mbrao::Parser.as_json(self, keys, options)
109
+ end
110
+
111
+ private
112
+
113
+ # Filters an attribute basing a set of locales.
114
+ #
115
+ # @param attribute [Object|HashWithIndifferentAccess] The desired attribute.
116
+ # @param locales [String|Array] The desired locales. Can include `*` to match all. If none are specified, the default mbrao locale will be used.
117
+ # @return [String|HashWithIndifferentAccess] Return the object for desired locales. If only one locale is available, then only a object is returned,
118
+ # else a `HashWithIndifferentAccess` with locales as keys.
119
+ def filter_attribute_for_locales(attribute, locales)
120
+ locales = ::Mbrao::Content.validate_locales(locales, self)
121
+
122
+ if attribute.is_a?(HashWithIndifferentAccess)
123
+ rv = attribute.reduce(HashWithIndifferentAccess.new) { |a, e| append_value_for_locale(a, e[0], locales, e[1]) }
124
+ rv.keys.length == 1 ? rv.values.first : rv
125
+ else
126
+ attribute
127
+ end
128
+ end
129
+
130
+ # Adds an value on a hash if enable for requested locales.
131
+ #
132
+ # @param hash [Hash] The hash to handle.
133
+ # @param locale [String] The list of locale (separated by commas) for which the value is available.
134
+ # @param locales [Array] The list of locale for which this value is requested. Can include `*` to match all. If none are specified, the default mbrao
135
+ # locale will be used.
136
+ # @param value [Object] The value to add.
137
+ # @return [Hash] Return the original hash.
138
+ def append_value_for_locale(hash, locale, locales, value)
139
+ locale.split(/\s*,\s*/).map(&:strip).each do |l|
140
+ hash[l] = value if locales.include?("*") || locales.include?(l)
141
+ end
142
+
143
+ hash
144
+ end
145
+ end
146
+ end
@@ -35,4 +35,4 @@ module Mbrao
35
35
  class Unimplemented < RuntimeError
36
36
  end
37
37
  end
38
- end
38
+ end
@@ -4,55 +4,61 @@
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
7
- # Generic interface to multiple Ruby template engines.
8
- module ActionView::Template::Handlers
9
- # Class for rendering mbrao contents in Rails.
10
- class MbraoTemplate
11
- # Returns a unique (singleton) instance of the template handler.
12
- #
13
- # @param force [Boolean] If to force recreation of the instance.
14
- # @return [MbraoTemplate] The unique (singleton) instance of the template handler.
15
- def self.instance(force = false)
16
- @instance = nil if force
17
- @instance ||= ActionView::Template::Handlers::MbraoTemplate.new
18
- end
7
+ # :nodoc:
8
+ module ActionView
9
+ # :nodoc:
10
+ class Template
11
+ # :nodoc:
12
+ module Handlers
13
+ # Class for rendering mbrao contents in Rails.
14
+ class MbraoTemplate
15
+ # Returns a unique (singleton) instance of the template handler.
16
+ #
17
+ # @param force [Boolean] If to force recreation of the instance.
18
+ # @return [MbraoTemplate] The unique (singleton) instance of the template handler.
19
+ def self.instance(force = false)
20
+ @instance = nil if force
21
+ @instance ||= ActionView::Template::Handlers::MbraoTemplate.new
22
+ end
19
23
 
20
- # Register Mbrao into Rails.
21
- def self.register
22
- ActionView::Template.register_template_handler("emt", instance) if defined?(ActionView) && defined?(Rails) && Rails.version =~ /^[34]/
23
- end
24
+ # Register Mbrao into Rails.
25
+ def self.register
26
+ ActionView::Template.register_template_handler("emt", instance) if defined?(ActionView) && defined?(Rails) && Rails.version =~ /^[34]/
27
+ end
24
28
 
25
- # Renders a template into a renderer context.
26
- #
27
- # @param renderer [Object] The renderer context.
28
- # @param template [String] The template to render.
29
- # @return [String] The rendered template.
30
- def render(renderer, template)
31
- content = ::Mbrao::Parser.parse(template)
32
- controller = renderer.controller
29
+ # Renders a template into a renderer context.
30
+ #
31
+ # @param renderer [Object] The renderer context.
32
+ # @param template [String] The template to render.
33
+ # @return [String] The rendered template.
34
+ def render(renderer, template)
35
+ content = ::Mbrao::Parser.parse(template)
36
+ controller = renderer.controller
33
37
 
34
- controller.instance_variable_set(:@mbrao_content, content)
35
- controller.define_singleton_method(:mbrao_content) { @mbrao_content }
36
- controller.class.send(:helper_method, :mbrao_content)
38
+ controller.instance_variable_set(:@mbrao_content, content)
39
+ controller.define_singleton_method(:mbrao_content) { @mbrao_content }
40
+ controller.class.send(:helper_method, :mbrao_content)
37
41
 
38
- ::Mbrao::Parser.render(content, {engine: content.metadata[:engine], locale: controller.params[:locale]})
39
- end
42
+ ::Mbrao::Parser.render(content, {engine: content.metadata[:engine], locale: controller.params[:locale]})
43
+ end
40
44
 
41
- # Declares support for streaming.
42
- #
43
- # @return [TrueClass] Declares support for streaming.
44
- def supports_streaming?
45
- true
46
- end
45
+ # Declares support for streaming.
46
+ #
47
+ # @return [TrueClass] Declares support for streaming.
48
+ def supports_streaming?
49
+ true
50
+ end
47
51
 
48
- # Method called to render a template.
49
- #
50
- # @param template [ActionView::Template] The template to render.
51
- # @return [String] A Ruby snippet to execute to render the template.
52
- def call(template)
53
- "ActionView::Template::Handlers::MbraoTemplate.instance.render(self, #{template.source.to_json})"
52
+ # Method called to render a template.
53
+ #
54
+ # @param template [ActionView::Template] The template to render.
55
+ # @return [String] A Ruby snippet to execute to render the template.
56
+ def call(template)
57
+ "ActionView::Template::Handlers::MbraoTemplate.instance.render(self, #{template.source.to_json})"
58
+ end
59
+ end
54
60
  end
55
61
  end
56
62
  end
57
63
 
58
- ActionView::Template::Handlers::MbraoTemplate.register
64
+ ActionView::Template::Handlers::MbraoTemplate.register
data/lib/mbrao/parser.rb CHANGED
@@ -6,164 +6,11 @@
6
6
 
7
7
  # A content parser and renderer with embedded metadata support.
8
8
  module Mbrao
9
- # Methods to allow class level access.
10
- module PublicInterface
11
- extend ActiveSupport::Concern
12
-
13
- # Class methods.
14
- #
15
- # @attribute default_locale
16
- # @return [String] The mbrao default locale.
17
- # @attribute parsing_engine
18
- # @return [String] The default parsing engine.
19
- # @attribute rendering_engine
20
- # @return [String] The default rendering engine.
21
- module ClassMethods
22
- attr_accessor :locale
23
- attr_accessor :parsing_engine
24
- attr_accessor :rendering_engine
25
-
26
- # Gets the default locale for mbrao.
27
- #
28
- # @return [String] The default locale.
29
- def locale
30
- attribute_or_default(@locale, "en")
31
- end
32
-
33
- # Gets the default parsing engine.
34
- #
35
- # @return [String] The default parsing engine.
36
- def parsing_engine
37
- attribute_or_default(@parsing_engine, :plain_text, :to_sym)
38
- end
39
-
40
- # Gets the default rendering engine.
41
- #
42
- # @return [String] The default rendering engine.
43
- def rendering_engine
44
- attribute_or_default(@rendering_engine, :html_pipeline, :to_sym)
45
- end
46
-
47
- # Parses a source text.
48
- #
49
- # @param content [Object] The content to parse.
50
- # @param options [Hash] A list of options for parsing.
51
- # @return [Content] The parsed data.
52
- def parse(content, options = {})
53
- instance.parse(content, options)
54
- end
55
-
56
- # Renders a content.
57
- #
58
- # @param content [Content] The content to parse.
59
- # @param options [Hash] A list of options for renderer.
60
- # @param context [Hash] A context for rendering.
61
- # @return [String] The rendered content.
62
- def render(content, options = {}, context = {})
63
- instance.render(content, options, context)
64
- end
65
-
66
- # Returns an object as a JSON compatible hash
67
- #
68
- # @param target [Object] The target to serialize.
69
- # @param keys [Array] The attributes to include in the serialization
70
- # @param options [Hash] Options to modify behavior of the serialization.
71
- # The only supported value are:
72
- #
73
- # * `:exclude`, an array of attributes to skip.
74
- # * `:exclude_empty`, if to exclude nil values. Default is `false`.
75
- # @return [Hash] An hash with all attributes.
76
- def as_json(target, keys, options = {})
77
- include_empty = !options[:exclude_empty].to_boolean
78
- exclude = options[:exclude].ensure_array(nil, true, true, true, :ensure_string)
79
- keys = keys.ensure_array(nil, true, true, true, :ensure_string)
80
-
81
- (keys - exclude).reduce({}) {|rv, key|
82
- value = target.send(key)
83
- value = value.as_json if value && value.respond_to?(:as_json) && !value.is_a?(Symbol)
84
- rv[key] = value if include_empty || value.present?
85
- rv
86
- }.deep_stringify_keys
87
- end
88
-
89
- # Instantiates a new engine for rendering or parsing.
90
- #
91
- # @param cls [String|Symbol|Object] If a `String` or a `Symbol`, then it will be the class of the engine.
92
- # @param type [Symbol] The type or engine. Can be `:parsing` or `:rendering`.
93
- # @return [Object] A new engine.
94
- def create_engine(cls, type = :parsing)
95
- begin
96
- type = :parsing if type != :rendering
97
- ::Lazier.find_class(cls, "::Mbrao::#{type.to_s.classify}Engines::%CLASS%").new
98
- rescue NameError
99
- raise Mbrao::Exceptions::UnknownEngine.new
100
- end
101
- end
102
-
103
- # Returns a unique (singleton) instance of the parser.
104
- #
105
- # @param force [Boolean] If to force recreation of the instance.
106
- # @return [Parser] The unique (singleton) instance of the parser.
107
- def instance(force = false)
108
- @instance = nil if force
109
- @instance ||= Mbrao::Parser.new
110
- end
111
-
112
- private
113
- # Returns an attribute or a default value.
114
- #
115
- # @param attr [Object ]The attribute to return.
116
- # @param default_value [Object] The value to return if `attr` is blank.
117
- # @param sanitizer [Symbol] An optional method to sanitize the returned value.
118
- def attribute_or_default(attr, default_value = nil, sanitizer = :ensure_string)
119
- rv = attr.present? ? attr : default_value
120
- rv = rv.send(sanitizer) if sanitizer
121
- rv
122
- end
123
- end
124
- end
125
-
126
- # Methods to perform validations.
127
- module Validations
128
- extend ActiveSupport::Concern
129
-
130
- # Class methods.
131
- module ClassMethods
132
- # Checks if the text is a valid email.
133
- #
134
- # @param text [String] The text to check.
135
- # @return [Boolean] `true` if the string is valid email, `false` otherwise.
136
- def is_email?(text)
137
- regex = /^([a-z0-9_\.\-\+]+)@([\da-z\.\-]+)\.([a-z\.]{2,6})$/i
138
- text.ensure_string.strip =~ regex
139
- end
140
-
141
- # Checks if the text is a valid URL.
142
- #
143
- # @param text [String] The text to check.
144
- # @return [Boolean] `true` if the string is valid URL, `false` otherwise.
145
- def is_url?(text)
146
- regex = /
147
- ^(
148
- ([a-z0-9\-]+:\/\/) #PROTOCOL
149
- (([\w-]+\.)?) # LOWEST TLD
150
- ([\w-]+) # 2nd LEVEL TLD
151
- (\.[a-z]+) # TOP TLD
152
- ((:\d+)?) # PORT
153
- ([\S|\?]*) # PATH, QUERYSTRING AND FRAGMENT
154
- )$
155
- /ix
156
-
157
- text.ensure_string.strip =~ regex
158
- end
159
- end
160
- end
161
-
162
9
  # A parser to handle pipelined content.
163
10
  #
164
11
  class Parser
165
- include Mbrao::PublicInterface
166
- include Mbrao::Validations
12
+ include Mbrao::ParserInterface
13
+ include Mbrao::ParserValidations
167
14
 
168
15
  # Parses a source text.
169
16
  #
@@ -187,30 +34,31 @@ module Mbrao
187
34
  end
188
35
 
189
36
  private
190
- # Sanitizes options for parsing.
191
- #
192
- # @param options [Hash] The options to sanitize.
193
- # @return [HashWithIndifferentAccess] The sanitized options.
194
- def sanitize_parsing_options(options)
195
- options = options.ensure_hash(:symbols)
196
37
 
197
- options[:engine] ||= Mbrao::Parser.parsing_engine
198
- options[:metadata] = options.fetch(:metadata, true).to_boolean
199
- options[:content] = options.fetch(:content, true).to_boolean
38
+ # Sanitizes options for parsing.
39
+ #
40
+ # @param options [Hash] The options to sanitize.
41
+ # @return [HashWithIndifferentAccess] The sanitized options.
42
+ def sanitize_parsing_options(options)
43
+ options = options.ensure_hash(:symbols)
44
+
45
+ options[:engine] ||= Mbrao::Parser.parsing_engine
46
+ options[:metadata] = options.fetch(:metadata, true).to_boolean
47
+ options[:content] = options.fetch(:content, true).to_boolean
200
48
 
201
- HashWithIndifferentAccess.new(options)
202
- end
49
+ HashWithIndifferentAccess.new(options)
50
+ end
203
51
 
204
- # Sanitizes options for rendering.
205
- #
206
- # @param options [Hash] The options to sanitize.
207
- # @return [HashWithIndifferentAccess] The sanitized options.
208
- def sanitize_rendering_options(options)
209
- options = options.ensure_hash(:symbols)
52
+ # Sanitizes options for rendering.
53
+ #
54
+ # @param options [Hash] The options to sanitize.
55
+ # @return [HashWithIndifferentAccess] The sanitized options.
56
+ def sanitize_rendering_options(options)
57
+ options = options.ensure_hash(:symbols)
210
58
 
211
- options[:engine] ||= Mbrao::Parser.rendering_engine
59
+ options[:engine] ||= Mbrao::Parser.rendering_engine
212
60
 
213
- HashWithIndifferentAccess.new(options)
214
- end
61
+ HashWithIndifferentAccess.new(options)
62
+ end
215
63
  end
216
- end
64
+ end