angry_mob_common_targets 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/LICENSE +21 -0
  2. data/README.md +38 -0
  3. data/lib/common_mob.rb +9 -0
  4. data/lib/common_mob/digest.rb +43 -0
  5. data/lib/common_mob/erb.rb +72 -0
  6. data/lib/common_mob/file.rb +55 -0
  7. data/lib/common_mob/patch.rb +51 -0
  8. data/lib/common_mob/resource_locator.rb +9 -0
  9. data/lib/common_mob/shell.rb +323 -0
  10. data/lib/common_mob/template.rb +23 -0
  11. data/lib/common_mob/version.rb +3 -0
  12. data/targets/crontab_patch.rb +37 -0
  13. data/targets/extract.rb +40 -0
  14. data/targets/fetch.rb +40 -0
  15. data/targets/files.rb +244 -0
  16. data/targets/git.rb +84 -0
  17. data/targets/group.rb +33 -0
  18. data/targets/packages.rb +94 -0
  19. data/targets/ruby.rb +13 -0
  20. data/targets/services.rb +184 -0
  21. data/targets/shell.rb +43 -0
  22. data/targets/user.rb +108 -0
  23. data/vendor/mustache/CONTRIBUTORS +9 -0
  24. data/vendor/mustache/HISTORY.md +135 -0
  25. data/vendor/mustache/LICENSE +20 -0
  26. data/vendor/mustache/README.md +405 -0
  27. data/vendor/mustache/Rakefile +103 -0
  28. data/vendor/mustache/benchmarks/complex.erb +15 -0
  29. data/vendor/mustache/benchmarks/complex.haml +12 -0
  30. data/vendor/mustache/benchmarks/helper.rb +20 -0
  31. data/vendor/mustache/benchmarks/simple.erb +5 -0
  32. data/vendor/mustache/benchmarks/speed.rb +78 -0
  33. data/vendor/mustache/bin/mustache +90 -0
  34. data/vendor/mustache/contrib/mustache-mode.el +278 -0
  35. data/vendor/mustache/contrib/mustache.vim +69 -0
  36. data/vendor/mustache/examples/hash.rb +16 -0
  37. data/vendor/mustache/examples/hash.yml +5 -0
  38. data/vendor/mustache/examples/projects.mustache +26 -0
  39. data/vendor/mustache/examples/projects.yml +28 -0
  40. data/vendor/mustache/examples/self.mustache +4 -0
  41. data/vendor/mustache/examples/self.yml +3 -0
  42. data/vendor/mustache/examples/simple.mustache +10 -0
  43. data/vendor/mustache/examples/simple.rb +24 -0
  44. data/vendor/mustache/lib/mustache.rb +358 -0
  45. data/vendor/mustache/lib/mustache/context.rb +108 -0
  46. data/vendor/mustache/lib/mustache/generator.rb +160 -0
  47. data/vendor/mustache/lib/mustache/parser.rb +230 -0
  48. data/vendor/mustache/lib/mustache/sinatra.rb +180 -0
  49. data/vendor/mustache/lib/mustache/template.rb +59 -0
  50. data/vendor/mustache/lib/mustache/version.rb +3 -0
  51. data/vendor/mustache/lib/rack/bug/panels/mustache_panel.rb +81 -0
  52. data/vendor/mustache/lib/rack/bug/panels/mustache_panel/mustache_extension.rb +27 -0
  53. data/vendor/mustache/lib/rack/bug/panels/mustache_panel/view.mustache +46 -0
  54. data/vendor/mustache/man/mustache.1 +180 -0
  55. data/vendor/mustache/man/mustache.1.html +204 -0
  56. data/vendor/mustache/man/mustache.1.ron +127 -0
  57. data/vendor/mustache/man/mustache.5 +576 -0
  58. data/vendor/mustache/man/mustache.5.html +415 -0
  59. data/vendor/mustache/man/mustache.5.ron +324 -0
  60. data/vendor/mustache/mustache.gemspec +32 -0
  61. data/vendor/mustache/test/autoloading_test.rb +52 -0
  62. data/vendor/mustache/test/fixtures/comments.mustache +1 -0
  63. data/vendor/mustache/test/fixtures/comments.rb +14 -0
  64. data/vendor/mustache/test/fixtures/complex_view.mustache +17 -0
  65. data/vendor/mustache/test/fixtures/complex_view.rb +34 -0
  66. data/vendor/mustache/test/fixtures/crazy_recursive.mustache +9 -0
  67. data/vendor/mustache/test/fixtures/crazy_recursive.rb +31 -0
  68. data/vendor/mustache/test/fixtures/delimiters.mustache +8 -0
  69. data/vendor/mustache/test/fixtures/delimiters.rb +23 -0
  70. data/vendor/mustache/test/fixtures/double_section.mustache +7 -0
  71. data/vendor/mustache/test/fixtures/double_section.rb +14 -0
  72. data/vendor/mustache/test/fixtures/escaped.mustache +1 -0
  73. data/vendor/mustache/test/fixtures/escaped.rb +14 -0
  74. data/vendor/mustache/test/fixtures/inner_partial.mustache +1 -0
  75. data/vendor/mustache/test/fixtures/inner_partial.txt +1 -0
  76. data/vendor/mustache/test/fixtures/inverted_section.mustache +7 -0
  77. data/vendor/mustache/test/fixtures/inverted_section.rb +14 -0
  78. data/vendor/mustache/test/fixtures/lambda.mustache +7 -0
  79. data/vendor/mustache/test/fixtures/lambda.rb +31 -0
  80. data/vendor/mustache/test/fixtures/namespaced.mustache +1 -0
  81. data/vendor/mustache/test/fixtures/namespaced.rb +25 -0
  82. data/vendor/mustache/test/fixtures/nested_objects.mustache +17 -0
  83. data/vendor/mustache/test/fixtures/nested_objects.rb +35 -0
  84. data/vendor/mustache/test/fixtures/node.mustache +8 -0
  85. data/vendor/mustache/test/fixtures/partial_with_module.mustache +3 -0
  86. data/vendor/mustache/test/fixtures/partial_with_module.rb +37 -0
  87. data/vendor/mustache/test/fixtures/passenger.conf +5 -0
  88. data/vendor/mustache/test/fixtures/passenger.rb +27 -0
  89. data/vendor/mustache/test/fixtures/recursive.mustache +4 -0
  90. data/vendor/mustache/test/fixtures/recursive.rb +14 -0
  91. data/vendor/mustache/test/fixtures/simple.mustache +5 -0
  92. data/vendor/mustache/test/fixtures/simple.rb +26 -0
  93. data/vendor/mustache/test/fixtures/template_partial.mustache +2 -0
  94. data/vendor/mustache/test/fixtures/template_partial.rb +18 -0
  95. data/vendor/mustache/test/fixtures/template_partial.txt +4 -0
  96. data/vendor/mustache/test/fixtures/unescaped.mustache +1 -0
  97. data/vendor/mustache/test/fixtures/unescaped.rb +14 -0
  98. data/vendor/mustache/test/fixtures/utf8.mustache +3 -0
  99. data/vendor/mustache/test/fixtures/utf8_partial.mustache +1 -0
  100. data/vendor/mustache/test/helper.rb +7 -0
  101. data/vendor/mustache/test/mustache_test.rb +536 -0
  102. data/vendor/mustache/test/parser_test.rb +54 -0
  103. data/vendor/mustache/test/partial_test.rb +168 -0
  104. metadata +167 -0
@@ -0,0 +1,108 @@
1
+ class Mustache
2
+ # A ContextMiss is raised whenever a tag's target can not be found
3
+ # in the current context if `Mustache#raise_on_context_miss?` is
4
+ # set to true.
5
+ #
6
+ # For example, if your View class does not respond to `music` but
7
+ # your template contains a `{{music}}` tag this exception will be raised.
8
+ #
9
+ # By default it is not raised. See Mustache.raise_on_context_miss.
10
+ class ContextMiss < RuntimeError; end
11
+
12
+ # A Context represents the context which a Mustache template is
13
+ # executed within. All Mustache tags reference keys in the Context.
14
+ class Context
15
+ # Expect to be passed an instance of `Mustache`.
16
+ def initialize(mustache)
17
+ @stack = [mustache]
18
+ end
19
+
20
+ # A {{>partial}} tag translates into a call to the context's
21
+ # `partial` method, which would be this sucker right here.
22
+ #
23
+ # If the Mustache view handling the rendering (e.g. the view
24
+ # representing your profile page or some other template) responds
25
+ # to `partial`, we call it and render the result.
26
+ def partial(name)
27
+ # Look for the first Mustache in the stack.
28
+ mustache = mustache_in_stack
29
+
30
+ # Call its `partial` method and render the result.
31
+ mustache.render(mustache.partial(name), self)
32
+ end
33
+
34
+ # Find the first Mustache in the stack. If we're being rendered
35
+ # inside a Mustache object as a context, we'll use that one.
36
+ def mustache_in_stack
37
+ @stack.detect { |frame| frame.is_a?(Mustache) }
38
+ end
39
+
40
+ # Adds a new object to the context's internal stack.
41
+ #
42
+ # Returns the Context.
43
+ def push(new)
44
+ @stack.unshift(new)
45
+ self
46
+ end
47
+ alias_method :update, :push
48
+
49
+ # Removes the most recently added object from the context's
50
+ # internal stack.
51
+ #
52
+ # Returns the Context.
53
+ def pop
54
+ @stack.shift
55
+ self
56
+ end
57
+
58
+ # Can be used to add a value to the context in a hash-like way.
59
+ #
60
+ # context[:name] = "Chris"
61
+ def []=(name, value)
62
+ push(name => value)
63
+ end
64
+
65
+ # Alias for `fetch`.
66
+ def [](name)
67
+ fetch(name, nil)
68
+ end
69
+
70
+ # Do we know about a particular key? In other words, will calling
71
+ # `context[key]` give us a result that was set. Basically.
72
+ def has_key?(key)
73
+ !!fetch(key)
74
+ rescue ContextMiss
75
+ false
76
+ end
77
+
78
+ # Similar to Hash#fetch, finds a value by `name` in the context's
79
+ # stack. You may specify the default return value by passing a
80
+ # second parameter.
81
+ #
82
+ # If no second parameter is passed (or raise_on_context_miss is
83
+ # set to true), will raise a ContextMiss exception on miss.
84
+ def fetch(name, default = :__raise)
85
+ @stack.each do |frame|
86
+ # Prevent infinite recursion.
87
+ next if frame == self
88
+
89
+ # Is this frame a hash?
90
+ hash = frame.respond_to?(:has_key?)
91
+
92
+ if hash && frame.has_key?(name)
93
+ return frame[name]
94
+ elsif hash && frame.has_key?(name.to_s)
95
+ return frame[name.to_s]
96
+ elsif !hash && frame.respond_to?(name)
97
+ return frame.__send__(name)
98
+ end
99
+ end
100
+
101
+ if default == :__raise || mustache_in_stack.raise_on_context_miss?
102
+ raise ContextMiss.new("Can't find #{name} in #{@stack.inspect}")
103
+ else
104
+ default
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,160 @@
1
+ class Mustache
2
+ # The Generator is in charge of taking an array of Mustache tokens,
3
+ # usually assembled by the Parser, and generating an interpolatable
4
+ # Ruby string. This string is considered the "compiled" template
5
+ # because at that point we're relying on Ruby to do the parsing and
6
+ # run our code.
7
+ #
8
+ # For example, let's take this template:
9
+ #
10
+ # Hi {{thing}}!
11
+ #
12
+ # If we run this through the Parser we'll get these tokens:
13
+ #
14
+ # [:multi,
15
+ # [:static, "Hi "],
16
+ # [:mustache, :etag, "thing"],
17
+ # [:static, "!\n"]]
18
+ #
19
+ # Now let's hand that to the Generator:
20
+ #
21
+ # >> puts Mustache::Generator.new.compile(tokens)
22
+ # "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
23
+ #
24
+ # You can see the generated Ruby string for any template with the
25
+ # mustache(1) command line tool:
26
+ #
27
+ # $ mustache --compile test.mustache
28
+ # "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
29
+ class Generator
30
+ # Options are unused for now but may become useful in the future.
31
+ def initialize(options = {})
32
+ @options = options
33
+ end
34
+
35
+ # Given an array of tokens, returns an interpolatable Ruby string.
36
+ def compile(exp)
37
+ "\"#{compile!(exp)}\""
38
+ end
39
+
40
+ # Given an array of tokens, converts them into Ruby code. In
41
+ # particular there are three types of expressions we are concerned
42
+ # with:
43
+ #
44
+ # :multi
45
+ # Mixed bag of :static, :mustache, and whatever.
46
+ #
47
+ # :static
48
+ # Normal HTML, the stuff outside of {{mustaches}}.
49
+ #
50
+ # :mustache
51
+ # Any Mustache tag, from sections to partials.
52
+ #
53
+ # To give you an idea of what you'll be dealing with take this
54
+ # template:
55
+ #
56
+ # Hello {{name}}
57
+ # You have just won ${{value}}!
58
+ # {{#in_ca}}
59
+ # Well, ${{taxed_value}}, after taxes.
60
+ # {{/in_ca}}
61
+ #
62
+ # If we run this through the Parser, we'll get back this array of
63
+ # tokens:
64
+ #
65
+ # [:multi,
66
+ # [:static, "Hello "],
67
+ # [:mustache, :etag, "name"],
68
+ # [:static, "\nYou have just won $"],
69
+ # [:mustache, :etag, "value"],
70
+ # [:static, "!\n"],
71
+ # [:mustache,
72
+ # :section,
73
+ # "in_ca",
74
+ # [:multi,
75
+ # [:static, "Well, $"],
76
+ # [:mustache, :etag, "taxed_value"],
77
+ # [:static, ", after taxes.\n"]]]]
78
+ def compile!(exp)
79
+ case exp.first
80
+ when :multi
81
+ exp[1..-1].map { |e| compile!(e) }.join
82
+ when :static
83
+ str(exp[1])
84
+ when :mustache
85
+ send("on_#{exp[1]}", *exp[2..-1])
86
+ else
87
+ raise "Unhandled exp: #{exp.first}"
88
+ end
89
+ end
90
+
91
+ # Callback fired when the compiler finds a section token. We're
92
+ # passed the section name and the array of tokens.
93
+ def on_section(name, content)
94
+ # Convert the tokenized content of this section into a Ruby
95
+ # string we can use.
96
+ code = compile(content)
97
+
98
+ # Compile the Ruby for this section now that we know what's
99
+ # inside the section.
100
+ ev(<<-compiled)
101
+ if v = ctx[#{name.to_sym.inspect}]
102
+ if v == true
103
+ #{code}
104
+ elsif v.is_a?(Proc)
105
+ v.call(#{code})
106
+ else
107
+ # Shortcut when passed non-array
108
+ v = [v] if v.respond_to?(:has_key?) || !v.respond_to?(:map)
109
+
110
+ v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
111
+ end
112
+ end
113
+ compiled
114
+ end
115
+
116
+ # Fired when we find an inverted section. Just like `on_section`,
117
+ # we're passed the inverted section name and the array of tokens.
118
+ def on_inverted_section(name, content)
119
+ # Convert the tokenized content of this section into a Ruby
120
+ # string we can use.
121
+ code = compile(content)
122
+
123
+ # Compile the Ruby for this inverted section now that we know
124
+ # what's inside.
125
+ ev(<<-compiled)
126
+ v = ctx[#{name.to_sym.inspect}]
127
+ if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
128
+ #{code}
129
+ end
130
+ compiled
131
+ end
132
+
133
+ # Fired when the compiler finds a partial. We want to return code
134
+ # which calls a partial at runtime instead of expanding and
135
+ # including the partial's body to allow for recursive partials.
136
+ def on_partial(name)
137
+ ev("ctx.partial(#{name.to_sym.inspect})")
138
+ end
139
+
140
+ # An unescaped tag.
141
+ def on_utag(name)
142
+ ev("ctx[#{name.to_sym.inspect}]")
143
+ end
144
+
145
+ # An escaped tag.
146
+ def on_etag(name)
147
+ ev("CGI.escapeHTML(ctx[#{name.to_sym.inspect}].to_s)")
148
+ end
149
+
150
+ # An interpolation-friendly version of a string, for use within a
151
+ # Ruby string.
152
+ def ev(s)
153
+ "#\{#{s}}"
154
+ end
155
+
156
+ def str(s)
157
+ s.inspect[1..-2]
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,230 @@
1
+ require 'strscan'
2
+
3
+ class Mustache
4
+ # The Parser is responsible for taking a string template and
5
+ # converting it into an array of tokens and, really, expressions. It
6
+ # raises SyntaxError if there is anything it doesn't understand and
7
+ # knows which sigil corresponds to which tag type.
8
+ #
9
+ # For example, given this template:
10
+ #
11
+ # Hi {{thing}}!
12
+ #
13
+ # Run through the Parser we'll get these tokens:
14
+ #
15
+ # [:multi,
16
+ # [:static, "Hi "],
17
+ # [:mustache, :etag, "thing"],
18
+ # [:static, "!\n"]]
19
+ #
20
+ # You can see the array of tokens for any template with the
21
+ # mustache(1) command line tool:
22
+ #
23
+ # $ mustache --tokens test.mustache
24
+ # [:multi, [:static, "Hi "], [:mustache, :etag, "thing"], [:static, "!\n"]]
25
+ class Parser
26
+ # A SyntaxError is raised when the Parser comes across unclosed
27
+ # tags, sections, illegal content in tags, or anything of that
28
+ # sort.
29
+ class SyntaxError < StandardError
30
+ def initialize(message, position)
31
+ @message = message
32
+ @lineno, @column, @line = position
33
+ @stripped_line = @line.strip
34
+ @stripped_column = @column - (@line.size - @line.lstrip.size)
35
+ end
36
+
37
+ def to_s
38
+ <<-EOF
39
+ #{@message}
40
+ Line #{@lineno}
41
+ #{@stripped_line}
42
+ #{' ' * @stripped_column}^
43
+ EOF
44
+ end
45
+ end
46
+
47
+ # After these types of tags, all whitespace will be skipped.
48
+ SKIP_WHITESPACE = [ '#', '^', '/' ]
49
+
50
+ # The content allowed in a tag name.
51
+ ALLOWED_CONTENT = /(\w|[?!\/-])*/
52
+
53
+ # These types of tags allow any content,
54
+ # the rest only allow ALLOWED_CONTENT.
55
+ ANY_CONTENT = [ '!', '=' ]
56
+
57
+ attr_reader :scanner, :result
58
+ attr_writer :otag, :ctag
59
+
60
+ # Accepts an options hash which does nothing but may be used in
61
+ # the future.
62
+ def initialize(options = {})
63
+ @options = {}
64
+ end
65
+
66
+ # The opening tag delimiter. This may be changed at runtime.
67
+ def otag
68
+ @otag ||= '{{'
69
+ end
70
+
71
+ # The closing tag delimiter. This too may be changed at runtime.
72
+ def ctag
73
+ @ctag ||= '}}'
74
+ end
75
+
76
+ # Given a string template, returns an array of tokens.
77
+ def compile(template)
78
+ if template.respond_to?(:encoding)
79
+ @encoding = template.encoding
80
+ template = template.dup.force_encoding("BINARY")
81
+ else
82
+ @encoding = nil
83
+ end
84
+
85
+ # Keeps information about opened sections.
86
+ @sections = []
87
+ @result = [:multi]
88
+ @scanner = StringScanner.new(template)
89
+
90
+ # Scan until the end of the template.
91
+ until @scanner.eos?
92
+ scan_tags || scan_text
93
+ end
94
+
95
+ if !@sections.empty?
96
+ # We have parsed the whole file, but there's still opened sections.
97
+ type, pos, result = @sections.pop
98
+ error "Unclosed section #{type.inspect}", pos
99
+ end
100
+
101
+ @result
102
+ end
103
+
104
+ # Find {{mustaches}} and add them to the @result array.
105
+ def scan_tags
106
+ # Scan until we hit an opening delimiter.
107
+ return unless @scanner.scan(regexp(otag))
108
+
109
+ # Since {{= rewrites ctag, we store the ctag which should be used
110
+ # when parsing this specific tag.
111
+ current_ctag = self.ctag
112
+ type = @scanner.scan(/#|\^|\/|=|!|<|>|&|\{/)
113
+ @scanner.skip(/\s*/)
114
+
115
+ # ANY_CONTENT tags allow any character inside of them, while
116
+ # other tags (such as variables) are more strict.
117
+ if ANY_CONTENT.include?(type)
118
+ r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
119
+ content = scan_until_exclusive(r)
120
+ else
121
+ content = @scanner.scan(ALLOWED_CONTENT)
122
+ end
123
+
124
+ # We found {{ but we can't figure out what's going on inside.
125
+ error "Illegal content in tag" if content.empty?
126
+
127
+ # Based on the sigil, do what needs to be done.
128
+ case type
129
+ when '#'
130
+ block = [:multi]
131
+ @result << [:mustache, :section, content, block]
132
+ @sections << [content, position, @result]
133
+ @result = block
134
+ when '^'
135
+ block = [:multi]
136
+ @result << [:mustache, :inverted_section, content, block]
137
+ @sections << [content, position, @result]
138
+ @result = block
139
+ when '/'
140
+ section, pos, result = @sections.pop
141
+ @result = result
142
+
143
+ if section.nil?
144
+ error "Closing unopened #{content.inspect}"
145
+ elsif section != content
146
+ error "Unclosed section #{section.inspect}", pos
147
+ end
148
+ when '!'
149
+ # ignore comments
150
+ when '='
151
+ self.otag, self.ctag = content.split(' ', 2)
152
+ when '>', '<'
153
+ @result << [:mustache, :partial, content]
154
+ when '{', '&'
155
+ # The closing } in unescaped tags is just a hack for
156
+ # aesthetics.
157
+ type = "}" if type == "{"
158
+ @result << [:mustache, :utag, content]
159
+ else
160
+ @result << [:mustache, :etag, content]
161
+ end
162
+
163
+ # Skip whitespace and any balancing sigils after the content
164
+ # inside this tag.
165
+ @scanner.skip(/\s+/)
166
+ @scanner.skip(regexp(type)) if type
167
+
168
+ # Try to find the closing tag.
169
+ unless close = @scanner.scan(regexp(current_ctag))
170
+ error "Unclosed tag"
171
+ end
172
+
173
+ # Skip whitespace following this tag if we need to.
174
+ @scanner.skip(/\s+/) if SKIP_WHITESPACE.include?(type)
175
+ end
176
+
177
+ # Try to find static text, e.g. raw HTML with no {{mustaches}}.
178
+ def scan_text
179
+ text = scan_until_exclusive(regexp(otag))
180
+
181
+ if text.nil?
182
+ # Couldn't find any otag, which means the rest is just static text.
183
+ text = @scanner.rest
184
+ # Mark as done.
185
+ @scanner.clear
186
+ end
187
+
188
+ text.force_encoding(@encoding) if @encoding
189
+
190
+ @result << [:static, text]
191
+ end
192
+
193
+ # Scans the string until the pattern is matched. Returns the substring
194
+ # *excluding* the end of the match, advancing the scan pointer to that
195
+ # location. If there is no match, nil is returned.
196
+ def scan_until_exclusive(regexp)
197
+ pos = @scanner.pos
198
+ if @scanner.scan_until(regexp)
199
+ @scanner.pos -= @scanner.matched.size
200
+ @scanner.pre_match[pos..-1]
201
+ end
202
+ end
203
+
204
+ # Returns [lineno, column, line]
205
+ def position
206
+ # The rest of the current line
207
+ rest = @scanner.check_until(/\n|\Z/).to_s.chomp
208
+
209
+ # What we have parsed so far
210
+ parsed = @scanner.string[0...@scanner.pos]
211
+
212
+ lines = parsed.split("\n")
213
+
214
+ [ lines.size, lines.last.size - 1, lines.last + rest ]
215
+ end
216
+
217
+ # Used to quickly convert a string into a regular expression
218
+ # usable by the string scanner.
219
+ def regexp(thing)
220
+ /#{Regexp.escape(thing)}/
221
+ end
222
+
223
+ # Raises a SyntaxError. The message should be the name of the
224
+ # error - other details such as line number and position are
225
+ # handled for you.
226
+ def error(message, pos = position)
227
+ raise SyntaxError.new(message, pos)
228
+ end
229
+ end
230
+ end