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,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