mbrao 1.4.4 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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,143 @@
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
+ # A content parser and renderer with embedded metadata support.
8
+ module Mbrao
9
+ # Methods to allow class level access.
10
+ module ParserInterface
11
+ extend ActiveSupport::Concern
12
+
13
+ # Class methods.
14
+ #
15
+ # @attribute 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
+ map_to_json(target, (keys - exclude), include_empty)
82
+ end
83
+
84
+ # Instantiates a new engine for rendering or parsing.
85
+ #
86
+ # @param cls [String|Symbol|Object] If a `String` or a `Symbol`, then it will be the class of the engine.
87
+ # @param type [Symbol] The type or engine. Can be `:parsing` or `:rendering`.
88
+ # @return [Object] A new engine.
89
+ def create_engine(cls, type = :parsing)
90
+ type = :parsing if type != :rendering
91
+ ::Lazier.find_class(cls, "::Mbrao::#{type.to_s.classify}Engines::%CLASS%").new
92
+ rescue NameError
93
+ raise Mbrao::Exceptions::UnknownEngine
94
+ end
95
+
96
+ # Returns a unique (singleton) instance of the parser.
97
+ #
98
+ # @param force [Boolean] If to force recreation of the instance.
99
+ # @return [Parser] The unique (singleton) instance of the parser.
100
+ def instance(force = false)
101
+ @instance = nil if force
102
+ @instance ||= Mbrao::Parser.new
103
+ end
104
+
105
+ private
106
+
107
+ # Returns an attribute or a default value.
108
+ #
109
+ # @param attr [Object ]The attribute to return.
110
+ # @param default_value [Object] The value to return if `attr` is blank.
111
+ # @param sanitizer [Symbol] An optional method to sanitize the returned value.
112
+ def attribute_or_default(attr, default_value = nil, sanitizer = :ensure_string)
113
+ rv = attr.present? ? attr : default_value
114
+ rv = rv.send(sanitizer) if sanitizer
115
+ rv
116
+ end
117
+
118
+ # Perform the mapping to JSON.
119
+ #
120
+ # @param target [Object] The target to serialize.
121
+ # @param keys [Array] The attributes to include in the serialization.
122
+ # @param include_empty [Boolean], if to include nil values.
123
+ # @return [Hash] An hash with all attributes.
124
+ def map_to_json(target, keys, include_empty)
125
+ keys.reduce({}) { |rv, key|
126
+ value = get_json_field(target, key)
127
+ rv[key] = value if include_empty || value.present?
128
+ rv
129
+ }.deep_stringify_keys
130
+ end
131
+
132
+ # Get a field as JSON.
133
+ #
134
+ # @param target [Object] The object containing the value.
135
+ # @param method [Symbol] The method containing the value.
136
+ def get_json_field(target, method)
137
+ value = target.send(method)
138
+ value = value.as_json if value && value.respond_to?(:as_json) && !value.is_a?(Symbol)
139
+ value
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,41 @@
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
+ # A content parser and renderer with embedded metadata support.
8
+ module Mbrao
9
+ # Methods to perform validations.
10
+ module ParserValidations
11
+ extend ActiveSupport::Concern
12
+
13
+ # Class methods.
14
+ module ClassMethods
15
+ # Checks if the text is a valid email.
16
+ #
17
+ # @param text [String] The text to check.
18
+ # @return [Boolean] `true` if the string is valid email, `false` otherwise.
19
+ def email?(text)
20
+ /^([a-z0-9_\.\-\+]+)@([\da-z\.\-]+)\.([a-z\.]{2,6})$/i.match(text.ensure_string.strip)
21
+ end
22
+
23
+ # Checks if the text is a valid URL.
24
+ #
25
+ # @param text [String] The text to check.
26
+ # @return [Boolean] `true` if the string is valid URL, `false` otherwise.
27
+ def url?(text)
28
+ %r{
29
+ ^(
30
+ ([a-z0-9\-]+:\/\/) #PROTOCOL
31
+ (([\w-]+\.)?) # LOWEST TLD
32
+ ([\w-]+) # 2nd LEVEL TLD
33
+ (\.[a-z]+) # TOP TLD
34
+ ((:\d+)?) # PORT
35
+ ([\S|\?]*) # PATH, QUERYSTRING AND FRAGMENT
36
+ )$
37
+ }ix.match(text.ensure_string.strip)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -15,7 +15,7 @@ module Mbrao
15
15
  # @param _options [Hash] Options to customize parsing.
16
16
  # @return [Array] An array of metadata and contents parts.
17
17
  def separate_components(_content, _options = {})
18
- raise Mbrao::Exceptions::Unimplemented.new
18
+ raise Mbrao::Exceptions::Unimplemented
19
19
  end
20
20
 
21
21
  # Parses metadata part and returns all valid metadata.
@@ -24,7 +24,7 @@ module Mbrao
24
24
  # @param _options [Hash] Options to customize parsing.
25
25
  # @return [Hash] All valid metadata for the content.
26
26
  def parse_metadata(_content, _options = {})
27
- raise Mbrao::Exceptions::Unimplemented.new
27
+ raise Mbrao::Exceptions::Unimplemented
28
28
  end
29
29
 
30
30
  # Filters content of a post by locale.
@@ -35,7 +35,7 @@ module Mbrao
35
35
  # @return [String|HashWithIndifferentAccess] Return the filtered content in the desired locales. If only one locale is required, then a `String` is
36
36
  # returned, else a `HashWithIndifferentAccess` with locales as keys.
37
37
  def filter_content(_content, _locales = [], _options = {})
38
- raise Mbrao::Exceptions::Unimplemented.new
38
+ raise Mbrao::Exceptions::Unimplemented
39
39
  end
40
40
 
41
41
  # Parses a content and return a {Content Content} object.
@@ -49,4 +49,4 @@ module Mbrao
49
49
  end
50
50
  end
51
51
  end
52
- end
52
+ end
@@ -17,10 +17,10 @@ module Mbrao
17
17
  def separate_components(content, options = {})
18
18
  metadata, content, scanner, start_tag, end_tag = prepare_for_separation(content, options)
19
19
 
20
- if scanner.scan_until(start_tag) then
20
+ if scanner.scan_until(start_tag)
21
21
  metadata = scanner.scan_until(end_tag)
22
22
 
23
- if metadata then
23
+ if metadata
24
24
  metadata = metadata.partition(end_tag).first
25
25
  content = scanner.rest.strip
26
26
  end
@@ -35,10 +35,15 @@ module Mbrao
35
35
  # @param options [Hash] Options to customize parsing.
36
36
  # @return [Hash] All valid metadata for the content.
37
37
  def parse_metadata(content, options = {})
38
- begin
39
- YAML.load(content)
40
- rescue Exception => e
41
- options[:default] ? options[:default] : (raise ::Mbrao::Exceptions::InvalidMetadata.new(e.to_s))
38
+ rv = YAML.load(content)
39
+ rv ||= {}
40
+ raise ArgumentError unless rv.is_a?(Hash)
41
+ rv
42
+ rescue => e
43
+ if options[:default]
44
+ options[:default]
45
+ else
46
+ raise ::Mbrao::Exceptions::InvalidMetadata, e.to_s
42
47
  end
43
48
  end
44
49
 
@@ -62,132 +67,142 @@ module Mbrao
62
67
  end
63
68
 
64
69
  private
65
- # Prepare arguments for separation.
66
- #
67
- # @param content [String] The content to separate.
68
- # @param options [Hash] The options to sanitize.
69
- # @return [Array] The sanitized arguments.
70
- def prepare_for_separation(content, options)
71
- content = content.ensure_string.strip
72
- meta_tags = sanitize_tags(options[:meta_tags], ["{{metadata}}", "{{/metadata}}"])
73
-
74
- [nil, content.ensure_string.strip, StringScanner.new(content), meta_tags.first, meta_tags.last]
75
- end
76
70
 
77
- # Sanitizes tag markers.
78
- #
79
- # @param tag [Array|String] The tag to sanitize.
80
- # @return [Array] Sanitized tags.
81
- def sanitize_tags(tag, default = ["---"])
82
- tag = tag.ensure_string.split(/\s*,\s*/).map(&:strip) if tag && !tag.is_a?(Array)
83
- (tag.present? ? tag : default).slice(0, 2).map {|t| /#{Regexp.quote(t).gsub("%ARGS%", "\\s*(?<args>[^\\n\\}]+,?)*")}/ }
84
- end
71
+ # Prepare arguments for separation.
72
+ #
73
+ # @param content [String] The content to separate.
74
+ # @param options [Hash] The options to sanitize.
75
+ # @return [Array] The sanitized arguments.
76
+ def prepare_for_separation(content, options)
77
+ content = content.ensure_string.strip
78
+ meta_tags = sanitize_tags(options[:meta_tags], ["{{metadata}}", "{{/metadata}}"])
79
+
80
+ [nil, content.ensure_string.strip, StringScanner.new(content), meta_tags.first, meta_tags.last]
81
+ end
85
82
 
86
- # Scans a text and content section.
87
- #
88
- # @param content [String] The string to scan
89
- # @param start_tag [Regexp] The tag to match for starting section.
90
- # @param end_tag [Regexp] The tag to match for ending section.
91
- def scan_content(content, start_tag, end_tag)
92
- rv = []
93
- scanner = StringScanner.new(content)
94
-
95
- # Begin scanning the string
96
- while !scanner.eos? do
97
- if scanner.exist?(start_tag) then # It may start an embedded content
98
- # Scan until the start tag, remove the tag from the match and then store to results.
99
- rv << [scanner.scan_until(start_tag).partition(start_tag).first, "*"]
100
-
101
- # Keep a reference to the start tag
102
- starting = scanner.matched
103
-
104
- # Now try to match the rightmost occurring closing tag
105
- embedded = parse_embedded_content(scanner, start_tag, end_tag)
106
-
107
- # Append results
108
- rv << get_embedded_content(starting, embedded, start_tag, end_tag)
109
- else # Append the rest to the result.
110
- rv << [scanner.rest, "*"]
111
- scanner.terminate
112
- end
113
- end
83
+ # Sanitizes tag markers.
84
+ #
85
+ # @param tag [Array|String] The tag to sanitize.
86
+ # @return [Array] Sanitized tags.
87
+ def sanitize_tags(tag, default = ["---"])
88
+ tag = tag.ensure_string.split(/\s*,\s*/).map(&:strip) if tag && !tag.is_a?(Array)
89
+ (tag.present? ? tag : default).slice(0, 2).map { |t| /#{Regexp.quote(t).gsub("%ARGS%", "\\s*(?<args>[^\\n\\}]+,?)*")}/ }
90
+ end
114
91
 
115
- rv
116
- end
92
+ # Scans a text and content section.
93
+ #
94
+ # @param content [String] The string to scan
95
+ # @param start_tag [Regexp] The tag to match for starting section.
96
+ # @param end_tag [Regexp] The tag to match for ending section.
97
+ # @return [String] The result of the scan.
98
+ def scan_content(content, start_tag, end_tag)
99
+ rv = []
100
+ scanner = StringScanner.new(content)
101
+
102
+ # Begin scanning the string
103
+ perform_scan(rv, scanner, start_tag, end_tag) until scanner.eos?
104
+
105
+ rv
106
+ end
117
107
 
118
- # Gets results for an embedded content.
119
- #
120
- # @param [String] starting The match starting expression.
121
- # @param [String] embedded The embedded contents.
122
- # @param start_tag [Regexp] The tag to match for starting section.
123
- # @param end_tag [Regexp] The tag to match for ending section.
124
- # @return [Array] An array which the first element is the list of valid contents and second is the list of valid locales.
125
- def get_embedded_content(starting, embedded, start_tag, end_tag)
126
- # Either we have some content or the content was not closed and therefore we ignore this tag.
127
- embedded.present? ? [scan_content(embedded, start_tag, end_tag), starting.match(start_tag)["args"]] : [starting, "*"]
108
+ # Perform a scan on the text and content.
109
+ #
110
+ # @param rv [String] The string where to put the results.
111
+ # @param scanner [StringScanner] The scanner to use.
112
+ # @param start_tag [Regexp] The tag to match for starting section.
113
+ # @param end_tag [Regexp] The tag to match for ending section.
114
+ def perform_scan(rv, scanner, start_tag, end_tag)
115
+ if scanner.exist?(start_tag) # It may start an embedded content
116
+ # Scan until the start tag, remove the tag from the match and then store to results.
117
+ rv << [scanner.scan_until(start_tag).partition(start_tag).first, "*"]
118
+
119
+ # Keep a reference to the start tag
120
+ starting = scanner.matched
121
+
122
+ # Now try to match the rightmost occurring closing tag and then append results
123
+ embedded = parse_embedded_content(scanner, start_tag, end_tag)
124
+
125
+ # Append results
126
+ rv << get_embedded_content(starting, embedded, start_tag, end_tag)
127
+ else # Append the rest to the result.
128
+ rv << [scanner.rest, "*"]
129
+ scanner.terminate
128
130
  end
131
+ end
129
132
 
130
- # Parses embedded content of a tag
131
- #
132
- # @param scanner [StringScanner] The scanner to use.
133
- # @param start_tag [Regexp] The tag to match for starting section.
134
- # @param end_tag [Regexp] The tag to match for ending section.
135
- # @return [String] The embedded content or `nil`, if the content was never closed.
136
- def parse_embedded_content(scanner, start_tag, end_tag)
137
- rv = ""
138
- balance = 1
139
- embedded_part = scanner.scan_until(end_tag)
140
-
141
- while balance > 0 && embedded_part do
142
- balance += embedded_part.scan(start_tag).count - 1 # -1 Because there is a closure
143
- embedded_part = embedded_part.partition(end_tag).first if balance == 0 || !scanner.exist?(end_tag) # This is the last occurrence.
144
- rv << embedded_part
145
- embedded_part = scanner.scan_until(end_tag) if balance > 0
146
- end
133
+ # Gets results for an embedded content.
134
+ #
135
+ # @param [String] starting The match starting expression.
136
+ # @param [String] embedded The embedded contents.
137
+ # @param start_tag [Regexp] The tag to match for starting section.
138
+ # @param end_tag [Regexp] The tag to match for ending section.
139
+ # @return [Array] An array which the first element is the list of valid contents and second is the list of valid locales.
140
+ def get_embedded_content(starting, embedded, start_tag, end_tag)
141
+ # Either we have some content or the content was not closed and therefore we ignore this tag.
142
+ embedded.present? ? [scan_content(embedded, start_tag, end_tag), starting.match(start_tag)["args"]] : [starting, "*"]
143
+ end
147
144
 
148
- rv
145
+ # Parses embedded content of a tag
146
+ #
147
+ # @param scanner [StringScanner] The scanner to use.
148
+ # @param start_tag [Regexp] The tag to match for starting section.
149
+ # @param end_tag [Regexp] The tag to match for ending section.
150
+ # @return [String] The embedded content or `nil`, if the content was never closed.
151
+ def parse_embedded_content(scanner, start_tag, end_tag)
152
+ rv = ""
153
+ balance = 1
154
+ embedded_part = scanner.scan_until(end_tag)
155
+
156
+ while balance > 0 && embedded_part
157
+ balance += embedded_part.scan(start_tag).count - 1 # -1 Because there is a closure
158
+ embedded_part = embedded_part.partition(end_tag).first if balance == 0 || !scanner.exist?(end_tag) # This is the last occurrence.
159
+ rv << embedded_part
160
+ embedded_part = scanner.scan_until(end_tag) if balance > 0
149
161
  end
150
162
 
151
- # Filters content by locale.
152
- #
153
- # @param content [Array] The content to filter. @see #scan_content.
154
- # @param locales [Array] The desired locales. Can include `*` to match all.
155
- # @return [String|nil] Return the filtered content or `nil` if the content must be hidden.
156
- def perform_filter_content(content, locales)
157
- content.map { |part|
158
- part_content = part[0]
159
- part_locales = parse_locales(part[1])
160
-
161
- if locales_valid?(locales, part_locales) then
162
- part_content.is_a?(Array) ? perform_filter_content(part_content, locales) : part_content
163
- else
164
- nil
165
- end
166
- }.compact.join("")
167
- end
163
+ rv
164
+ end
168
165
 
169
- # Parses locales of a content.
170
- #
171
- # @param locales [String] The desired locales. Can include `*` to match all. Note that `!*` is ignored.
172
- # @return [Hash] An hash with valid and invalid locales.
173
- def parse_locales(locales)
174
- types = locales.split(/\s*,\s*/).map(&:strip).group_by {|l| l !~ /^!/ ? "valid" : "invalid" }
175
- types["valid"] ||= []
176
- types["invalid"] = types.fetch("invalid", []).reject {|l| l == "!*" }.map {|l| l.gsub(/^!/, "") }
177
- types
178
- end
166
+ # Filters content by locale.
167
+ #
168
+ # @param content [Array] The content to filter. @see #scan_content.
169
+ # @param locales [Array] The desired locales. Can include `*` to match all.
170
+ # @return [String|nil] Return the filtered content or `nil` if the content must be hidden.
171
+ def perform_filter_content(content, locales)
172
+ content.map { |part|
173
+ part_content = part[0]
174
+ part_locales = parse_locales(part[1])
175
+
176
+ if locales_valid?(locales, part_locales)
177
+ part_content.is_a?(Array) ? perform_filter_content(part_content, locales) : part_content
178
+ else
179
+ nil
180
+ end
181
+ }.compact.join("")
182
+ end
179
183
 
180
- # Checks if all locales in a list are valid for a part.
181
- #
182
- # @param locales [Array] The desired locales. Can include `*` to match all.
183
- # @param part_locales[Hash] An hash with valid and invalid locales.
184
- # @return [Boolean] `true` if the locales are valid, `false` otherwise.
185
- def locales_valid?(locales, part_locales)
186
- valid = part_locales["valid"]
187
- invalid = part_locales["invalid"]
184
+ # Parses locales of a content.
185
+ #
186
+ # @param locales [String] The desired locales. Can include `*` to match all. Note that `!*` is ignored.
187
+ # @return [Hash] An hash with valid and invalid locales.
188
+ def parse_locales(locales)
189
+ types = locales.split(/\s*,\s*/).map(&:strip).group_by { |l| l !~ /^!/ ? "valid" : "invalid" }
190
+ types["valid"] ||= []
191
+ types["invalid"] = types.fetch("invalid", []).reject { |l| l == "!*" }.map { |l| l.gsub(/^!/, "") }
192
+ types
193
+ end
188
194
 
189
- locales.include?("*") || valid.include?("*") || ((valid.empty? || (locales & valid).present?) && (locales & invalid).blank?)
190
- end
195
+ # Checks if all locales in a list are valid for a part.
196
+ #
197
+ # @param locales [Array] The desired locales. Can include `*` to match all.
198
+ # @param part_locales[Hash] An hash with valid and invalid locales.
199
+ # @return [Boolean] `true` if the locales are valid, `false` otherwise.
200
+ def locales_valid?(locales, part_locales)
201
+ valid = part_locales["valid"]
202
+ invalid = part_locales["invalid"]
203
+
204
+ locales.include?("*") || valid.include?("*") || ((valid.empty? || (locales & valid).present?) && (locales & invalid).blank?)
205
+ end
191
206
  end
192
207
  end
193
- end
208
+ end