sublime_dsl 0.1.1

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +136 -0
  3. data/Rakefile +248 -0
  4. data/SYNTAX.md +927 -0
  5. data/bin/subdsl +4 -0
  6. data/lib/sublime_dsl/cli/export.rb +134 -0
  7. data/lib/sublime_dsl/cli/import.rb +143 -0
  8. data/lib/sublime_dsl/cli.rb +125 -0
  9. data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
  10. data/lib/sublime_dsl/core_ext/string.rb +129 -0
  11. data/lib/sublime_dsl/core_ext.rb +4 -0
  12. data/lib/sublime_dsl/sublime_text/command.rb +157 -0
  13. data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
  14. data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
  15. data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
  16. data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
  17. data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
  18. data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
  19. data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
  20. data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
  21. data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
  22. data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
  23. data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
  24. data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
  25. data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
  26. data/lib/sublime_dsl/sublime_text/package.rb +96 -0
  27. data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
  28. data/lib/sublime_dsl/sublime_text.rb +48 -0
  29. data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
  30. data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
  31. data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
  32. data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
  33. data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
  34. data/lib/sublime_dsl/textmate/grammar.rb +252 -0
  35. data/lib/sublime_dsl/textmate/plist.rb +141 -0
  36. data/lib/sublime_dsl/textmate/preference.rb +301 -0
  37. data/lib/sublime_dsl/textmate/snippet.rb +437 -0
  38. data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
  39. data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
  40. data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
  41. data/lib/sublime_dsl/textmate/theme.rb +364 -0
  42. data/lib/sublime_dsl/textmate.rb +9 -0
  43. data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
  44. data/lib/sublime_dsl/tools/console.rb +74 -0
  45. data/lib/sublime_dsl/tools/helpers.rb +152 -0
  46. data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
  47. data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
  48. data/lib/sublime_dsl/tools/value_equality.rb +37 -0
  49. data/lib/sublime_dsl/tools/xml.rb +66 -0
  50. data/lib/sublime_dsl/tools.rb +66 -0
  51. data/lib/sublime_dsl.rb +23 -0
  52. metadata +145 -0
@@ -0,0 +1,437 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+
6
+ class Snippet
7
+
8
+ # Returns a Snippet read from +file+.
9
+ def self.import(file)
10
+ Importer.for(file).snippet
11
+ end
12
+
13
+ # Hash { attributeName => attribute_name }
14
+ def self.to_snake_map
15
+ @to_snake_map ||= Hash[
16
+ %w(
17
+ name content scope tabTrigger keyEquivalent
18
+ semanticClass uuid bundleUUID
19
+ ).map { |a| [a, a.snake_case] }
20
+ ]
21
+ end
22
+
23
+ include CustomBaseName
24
+
25
+ attr_accessor :name, :content, :tab_trigger, :scope
26
+ attr_accessor :key_equivalent, :semantic_class, :uuid, :bundle_uuid # TextMate only
27
+ attr_writer :file_format
28
+ attr_reader :warnings
29
+
30
+ def initialize
31
+ @name = nil
32
+ @content = nil
33
+ @tab_trigger = nil
34
+ @scope = nil
35
+
36
+ @key_equivalent = nil
37
+ @semantic_class = nil
38
+ @uuid = nil
39
+ @bundle_uuid = nil
40
+
41
+ @file_format = nil
42
+ @warnings = []
43
+ end
44
+
45
+ def file_format
46
+ @file_format || :sublime_text
47
+ end
48
+
49
+ def complete!
50
+
51
+ # assign name from the base name if not given
52
+ unless name
53
+ if @basename
54
+ warnings << 'name assigned from the file name'
55
+ @name = basename
56
+ @basename = nil
57
+ else
58
+ raise Error, 'the snippet name is required'
59
+ end
60
+ end
61
+
62
+ tab_trigger || key_equivalent or
63
+ warnings << 'no tab trigger nor key equivalent'
64
+
65
+ warnings.each { |w| warn "snippet #{name}: #{w}" }
66
+
67
+ # remove spaces on empty lines
68
+ @content.gsub!(/^[ \t]+$/, '')
69
+
70
+ end
71
+
72
+ alias to_s name
73
+
74
+ include Tools::StableInspect
75
+
76
+ def to_dsl(default_scope)
77
+
78
+ options = ''
79
+
80
+ case scope
81
+ when NilClass
82
+ warn "scope missing for snippet #{name.to_source}, set to '#{default_scope}'"
83
+ warnings << 'missing scope, will be default_scope'
84
+ when default_scope
85
+ else
86
+ options << ", scope: #{scope.to_source}"
87
+ end
88
+
89
+ options << ", semantic_class: #{semantic_class.to_source}" if semantic_class
90
+ options << ", uuid: #{uuid.to_source}" if uuid
91
+ options << ", bundle_uuid: #{bundle_uuid.to_source}" if bundle_uuid
92
+ options << dsl_file_arg
93
+
94
+ dsl = warnings.map { |w| " # FIXME: #{w}\n" }.join
95
+
96
+ if tab_trigger
97
+ start = "tab #{tab_trigger.to_source}"
98
+ options << ", key_equivalent: #{key_equivalent.inspect_dq}" if key_equivalent
99
+ elsif key_equivalent
100
+ start = "key #{key_equivalent.inspect_dq}"
101
+ else
102
+ start = 'key nil'
103
+ end
104
+
105
+ if content =~ /[ \t]$/
106
+ dsl << " #{start}, #{name.to_source}, #{content.inspect_dq}#{options}\n"
107
+ else
108
+ # TODO: use the ruby heredoc mnemonic from default_scope (CPP, RUBY, XML, etc.)
109
+ dsl << " #{start}, #{name.to_s.to_source}, <<-'TXT'#{options}\n"
110
+ dsl << content << "\nTXT\n"
111
+ end
112
+
113
+ dsl
114
+ end
115
+
116
+ def export(dir)
117
+ if file_format == :textmate
118
+ file = "#{dir}/#{basename}.tmSnippet"
119
+ content = to_plist
120
+ else
121
+ file = "#{dir}/#{basename}.sublime-snippet"
122
+ content = to_xml
123
+ end
124
+ File.open(file, 'wb:utf-8') { |f| f.write content }
125
+ end
126
+
127
+ def to_xml
128
+ <<-XML
129
+ <snippet>
130
+ \t<content>#{c(content)}</content>
131
+ \t<tabTrigger>#{h(tab_trigger)}</tabTrigger>
132
+ \t<scope>#{h(scope)}</scope>
133
+ \t<description>#{h(name)}</description>
134
+ </snippet>
135
+ XML
136
+ end
137
+
138
+ def to_plist
139
+ h = {}
140
+ Snippet.to_snake_map.each_pair do |camel, snake|
141
+ value = send(snake)
142
+ h[camel] = value if value
143
+ end
144
+ PList.dump(h)
145
+ end
146
+
147
+ def h(text)
148
+ text.html_escape(false)
149
+ end
150
+
151
+ # HACK: return <![CDATA[#{content}]]>, except if +content+ is itself <![CDATA[...
152
+ def c(content)
153
+ content =~ /<!\[CDATA\[/ ? h(content) : "<![CDATA[#{content}]]>"
154
+ end
155
+
156
+ ##
157
+ # Abstract importer
158
+
159
+ class Importer
160
+
161
+ # Return a concrete importer for +file+.
162
+ def self.for(file)
163
+ case File.extname(file)
164
+ when '.tmSnippet'
165
+ PListReader.new(file)
166
+ when '.sublime-snippet'
167
+ XMLReader.new(file)
168
+ else
169
+ raise Error, "unknown snippet file format: #{file}"
170
+ end
171
+ end
172
+
173
+ attr_reader :file, :snippet
174
+
175
+ def initialize(file)
176
+ @file = file
177
+ @snippet = Snippet.new
178
+ load
179
+ @snippet.basename = File.basename(file, File.extname(file))
180
+ @snippet.complete!
181
+ end
182
+
183
+ # Load the content of #file into #snippet
184
+ def load
185
+ raise NotImplementedError
186
+ end
187
+
188
+ end
189
+
190
+ ##
191
+ # TextMate format importer
192
+
193
+ class PListReader < Importer
194
+
195
+ def load
196
+ snippet.file_format = :textmate
197
+ p = read_plist(file)
198
+ snippet.tab_trigger = p.delete('tabTrigger')
199
+ snippet.name = p.delete('name')
200
+ snippet.content = p.delete('content')
201
+ snippet.scope = p.delete('scope')
202
+ snippet.key_equivalent = p.delete('keyEquivalent')
203
+ snippet.semantic_class = p.delete('semanticClass')
204
+ snippet.uuid = p.delete('uuid')
205
+ snippet.bundle_uuid = p.delete('bundleUUID')
206
+ p.empty? or warn "unexpected keys in #{file}: #{p.inspect}"
207
+ end
208
+
209
+ def read_plist(file)
210
+ text = File.read(file, encoding: 'utf-8')
211
+
212
+ # replace forbidden control characters (given in keyEquivalent)
213
+ h = {}
214
+ text.gsub!(Tools::XML::FORBIDDEN_CHARS_RE) do |c|
215
+ h[c] = Tools::XML::FORBIDDEN_CHARS_MAP[c]
216
+ end
217
+ h.each_pair do |c, rep|
218
+ snippet.warnings << "illegal XML character #{c.inspect} replaced by '#{rep}'"
219
+ end
220
+
221
+ PList.load(text)
222
+ end
223
+
224
+ end
225
+
226
+ ##
227
+ # Sublime Text format importer
228
+
229
+ class XMLReader < Importer
230
+
231
+ def load
232
+ snippet.file_format = :sublime_text
233
+
234
+ doc = File.open(file, 'r:utf-8') { |f| Tools::XML.load(f) }
235
+ # <snippet>
236
+ # <content><![CDATA[all? { |${1:e}| $0 }]]></content>
237
+ # <tabTrigger>all</tabTrigger>
238
+ # <scope>source.ruby</scope>
239
+ # <description>all? { |e| .. }</description>
240
+ # </snippet>
241
+ nodes = doc.children.reject { |n| n.comment? }
242
+ root = nodes.first
243
+ nodes.length == 1 && root.name == 'snippet' or
244
+ raise Error, "#{file}: invalid root, expected <snippet>"
245
+
246
+ root.children.each do |node|
247
+ node.attributes.empty? or
248
+ raise Error, "#{file}: unexpected attributes for #{node.name}: #{node.attributes.inspect}"
249
+ next if node.children.empty?
250
+ node.children.length == 1 && (node.children.first.text? || node.children.first.cdata?) or
251
+ raise Error, "#{file}: unexpected children for #{node.name}: #{node.children.inspect}"
252
+ if node.name == 'description'
253
+ snippet.name = node.text
254
+ else
255
+ method = Snippet.to_snake_map[node.name]
256
+ if method
257
+ snippet.send "#{method}=", node.text
258
+ else
259
+ warn "snippet attribute '#{node.name}' ignored: " << file
260
+ end
261
+ end
262
+ end
263
+
264
+ end
265
+
266
+ end
267
+
268
+ ##
269
+ # DSL reader for a collection of snippets.
270
+
271
+ class DSLReader
272
+
273
+ def initialize(file = nil)
274
+ @snippets = []
275
+ @default_scope = nil
276
+ @file_format = nil
277
+ @in_snippets = false
278
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
279
+ end
280
+
281
+ def _snippets
282
+ @snippets
283
+ end
284
+
285
+ def snippets(&block)
286
+ @in_snippets and raise Error, "'snippets' blocks cannot be nested"
287
+ @in_snippets = true
288
+ instance_eval(&block)
289
+ @in_snippets = false
290
+ end
291
+
292
+ def method_missing(sym, *args, &block)
293
+ raise Error, "'#{sym}' is not a snippet DSL statement"
294
+ end
295
+
296
+ def default_scope(scope)
297
+ ensure_context __method__
298
+ @default_scope = scope
299
+ end
300
+
301
+ def file_format(format)
302
+ ensure_context __method__
303
+ if format
304
+ format = format.to_sym
305
+ [:textmate, :sublime_text].include?(format) or
306
+ raise Error, "invalid snippet file format: #{format.inspect}"
307
+ end
308
+ @file_format = format
309
+ end
310
+
311
+ def tab(tab_trigger, name, content, options = {})
312
+ ensure_context __method__
313
+ _snippets << new_snippet(:tab_trigger=, tab_trigger, name, content, options)
314
+ end
315
+
316
+ def key(key_equivalent, name, content, options = {})
317
+ ensure_context __method__
318
+ _snippets << new_snippet(:key_equivalent=, key_equivalent, name, content, options)
319
+ end
320
+
321
+ private
322
+
323
+ def ensure_context(name)
324
+ @in_snippets or raise Error, "'#{name}' is invalid outside of 'snippets' blocks"
325
+ end
326
+
327
+ def new_snippet(method, arg, name, content, options)
328
+ snippet = Snippet.new
329
+ snippet.send method, arg
330
+ snippet.name = name
331
+ snippet.content = content.sub(/\n\z/, '')
332
+ snippet.scope = options.delete(:scope) || @default_scope
333
+ snippet.file_format = @file_format
334
+ if (k = options.delete(:key_equivalent))
335
+ if snippet.key_equivalent && snippet.key_equivalent != k
336
+ warn "key_equivalent option given for 'key' snippet: #{snippet}"
337
+ else
338
+ snippet.key_equivalent = k
339
+ end
340
+ end
341
+ if (t = options.delete(:tab_trigger))
342
+ if snippet.tab_trigger && snippet.tab_trigger != t
343
+ warn "tab_trigger option given for 'tab' snippet: #{snippet}"
344
+ else
345
+ snippet.tab_trigger = t
346
+ end
347
+ end
348
+ snippet.semantic_class = options.delete(:semantic_class)
349
+ snippet.uuid = options.delete(:uuid)
350
+ snippet.bundle_uuid = options.delete(:bundle_uuid)
351
+ snippet.basename = options.delete(:file)
352
+
353
+ options.empty? or warn "invalid snippet options: #{options.inspect}"
354
+ snippet.complete!
355
+
356
+ snippet
357
+ end
358
+
359
+ end
360
+
361
+ ##
362
+ # DSL writer for a collection of snippets.
363
+
364
+ class DSLWriter
365
+
366
+ attr_reader :snippets
367
+
368
+ def initialize(snippets)
369
+ @snippets = snippets
370
+ end
371
+
372
+ def write(file)
373
+ return if snippets.empty?
374
+ tm, st = snippets.partition { |s| s.file_format == :textmate }
375
+ File.open(file, 'wb:utf-8') do |f|
376
+ f.write dsl_header
377
+ f.write dsl_block(tm)
378
+ f.write dsl_block(st)
379
+ f.write dsl_footer
380
+ end
381
+ end
382
+
383
+ private
384
+
385
+ def dsl_header
386
+ <<-HEADER.dedent
387
+ # encoding: utf-8
388
+
389
+ snippets do
390
+
391
+ default_scope #{default_scope.to_source}
392
+ HEADER
393
+ end
394
+
395
+ def dsl_block(snippets)
396
+ return '' if snippets.empty?
397
+ dsl = ''
398
+ dsl << " file_format :textmate\n" if snippets.first.file_format == :textmate
399
+ sort(snippets).each do |s|
400
+ dsl << "\n#{s.to_dsl(default_scope)}"
401
+ end
402
+
403
+ dsl
404
+ end
405
+
406
+ def dsl_footer
407
+ "\nend"
408
+ end
409
+
410
+ def default_scope
411
+ @default_scope ||= most_frequent_scope
412
+ end
413
+
414
+ def most_frequent_scope
415
+ scope = nil
416
+ max_count = 0
417
+ sort(self.snippets).group_by(&:scope).each_pair do |s,v|
418
+ if s && v.length > max_count
419
+ scope = s
420
+ max_count = v.length
421
+ end
422
+ end
423
+
424
+ scope
425
+ end
426
+
427
+ # Returns +snippets+ sorted by tab_trigger and name.
428
+ def sort(snippets)
429
+ snippets.sort_by { |s| [s.tab_trigger.to_s.downcase, s.name.to_s.downcase] }
430
+ end
431
+
432
+ end
433
+
434
+ end
435
+
436
+ end
437
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+ class Theme
6
+
7
+ class DSLReader
8
+
9
+ def initialize(file = nil)
10
+ @themes = []
11
+ @current_theme = nil
12
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
13
+ end
14
+
15
+ def _themes
16
+ @themes
17
+ end
18
+
19
+ def method_missing(sym, *args, &block)
20
+ raise Error, "'#{sym}' is not a valid Theme DSL statement"
21
+ end
22
+
23
+ def bold; 'bold' end
24
+ def italic; 'italic' end
25
+ def underline; 'underline' end
26
+
27
+ def theme(name, options={}, &block)
28
+ @current_theme and raise Error, "'theme' blocks cannot be nested"
29
+ @current_theme = Theme.new(name)
30
+ @current_theme.basename = options.delete(:file)
31
+ options.empty? or warn "invalid options: #{options.inspect}"
32
+ instance_eval(&block)
33
+ @themes << @current_theme
34
+ @current_theme = nil
35
+ end
36
+
37
+ def author(value)
38
+ ensure_context __method__
39
+ @current_theme.author = value
40
+ end
41
+
42
+ def uuid(value)
43
+ ensure_context __method__
44
+ @current_theme.uuid = value
45
+ end
46
+
47
+ def license(text)
48
+ @current_theme.license = text
49
+ end
50
+
51
+ def base_colors(options = {})
52
+ ensure_context __method__
53
+ @current_theme.base_colors.merge! options
54
+ end
55
+
56
+ def item(name, scope, *attributes)
57
+ ensure_context __method__
58
+ item = Item.new(name, scope)
59
+ attributes.each do |a|
60
+ if a.is_a? Hash
61
+ a.each_pair do |k, v|
62
+ if k == :back
63
+ item.background = v
64
+ else
65
+ raise Error, "invalid item option: #{k.inspect} => #{v.inspect}"
66
+ end
67
+ end
68
+ elsif %w(bold italic underline).include?(a)
69
+ item.send "#{a}=", true
70
+ elsif a.start_with? '#'
71
+ item.foreground = a
72
+ else
73
+ raise Error, "invalid item attribute: #{a.inspect}"
74
+ end
75
+ end
76
+ @current_theme.add_item item
77
+ end
78
+
79
+ def ensure_context(name)
80
+ @current_theme or raise Error, "#{name} is invalid outside a 'theme' block"
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+ class Theme
6
+
7
+ class Item
8
+
9
+ attr_reader :name, :scope
10
+ attr_reader :background, :foreground
11
+ attr_accessor :bold, :italic, :underline
12
+
13
+ def initialize(name, scope)
14
+ @name = name.nil? || name.empty? ? nil : name
15
+ @scope = scope.nil? || scope.empty? ? nil : scope.strip.gsub(/\s+/m, ' ')
16
+ @background = nil
17
+ @foreground = nil
18
+ @bold = nil
19
+ @italic = nil
20
+ @underline = nil
21
+ end
22
+
23
+ def background=(color)
24
+ @background = color && color.upcase
25
+ end
26
+
27
+ def foreground=(color)
28
+ @foreground = color && color.upcase
29
+ end
30
+
31
+ def font_style
32
+ style = []
33
+ %w(bold italic underline).each { |a| style << a if self.send(a) }
34
+ style.empty? ? nil : style.join(' ')
35
+ end
36
+
37
+ def font_style=(string)
38
+ string.split(/\s+/).each do |attr|
39
+ case attr
40
+ when 'bold' then @bold = true
41
+ when 'italic' then @italic = true
42
+ when 'underline' then @underline = true
43
+ else raise Error, "unknown font_style: #{attr.inspect}"
44
+ end
45
+ end
46
+ end
47
+
48
+ def colors
49
+ [background, foreground].compact
50
+ end
51
+
52
+ def to_dsl(colors_hash)
53
+ scope_arg = scope.to_s.length > 70 ? '<<-SCOPE' : scope.to_s.to_source
54
+ dsl = ''
55
+ dsl << "# FIXME: no name:\n" unless name
56
+ dsl << "# FIXME: no scope:\n" unless scope
57
+ dsl << "item #{name.to_s.to_source}, #{scope_arg}"
58
+ dsl << ", #{colors_hash[foreground]}" if foreground
59
+ dsl << ", bold" if bold
60
+ dsl << ", italic" if italic
61
+ dsl << ", underline" if underline
62
+ dsl << ", back: #{colors_hash[background]}" if background
63
+ if scope_arg == '<<-SCOPE'
64
+ dsl << "\n" << scope.wrap.indent(2)
65
+ dsl << "\nSCOPE"
66
+ end
67
+ dsl
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+ class Theme
6
+
7
+ class PListWriter
8
+
9
+ attr_reader :theme
10
+ attr_reader :root
11
+
12
+ def initialize(theme)
13
+ @theme = theme
14
+ @root = {}
15
+ convert_theme
16
+ end
17
+
18
+ def export(file)
19
+ PList.export(root, file)
20
+ end
21
+
22
+ private
23
+
24
+ def convert_theme
25
+
26
+ root['name'] = theme.name
27
+ root['author'] = theme.author if theme.author
28
+ root['uuid'] = theme.uuid
29
+ root['license'] = theme.license.rstrip << "\n\t" if theme.license
30
+
31
+ base_colors = {}
32
+ theme.base_colors.each_pair { |k,v| base_colors[k.to_s.camel_case] = v }
33
+
34
+ items = theme.items.map do |e|
35
+ h = {}
36
+ h['name'] = e.name if e.name
37
+ h['scope'] = e.scope if e.scope
38
+ h['settings'] = a = {}
39
+ a['foreground'] = e.foreground if e.foreground
40
+ a['background'] = e.background if e.background
41
+ a['fontStyle'] = e.font_style if e.font_style
42
+ h
43
+ end
44
+
45
+ root['settings'] = [{ 'settings' => base_colors }] + items
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
53
+ end