parsejs 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +5 -0
- data/README.markdown +85 -0
- data/Rakefile +15 -0
- data/lib/parsejs.rb +14 -0
- data/lib/parsejs/ast.rb +84 -0
- data/lib/parsejs/docs.rb +237 -0
- data/lib/parsejs/grammar.kpeg +575 -0
- data/lib/parsejs/grammar.kpeg.rb +9460 -0
- data/lib/parsejs/scope.rb +183 -0
- data/lib/parsejs/stringifier.rb +450 -0
- data/lib/parsejs/version.rb +3 -0
- data/lib/parsejs/visitor.rb +257 -0
- data/parsejs.gemspec +24 -0
- data/spec/fixtures/jquery-1.6.js +8982 -0
- data/spec/fixtures/jquery-1.7.js +9289 -0
- data/spec/fixtures/jquery-ajax.js +1000 -0
- data/spec/fixtures/jquery-attributes.js +650 -0
- data/spec/fixtures/jquery-traversing.js +330 -0
- data/spec/fixtures/metamorph.js +324 -0
- data/spec/fixtures/sizzle.js +1442 -0
- data/spec/fixtures/sproutcore-core.js +195 -0
- data/spec/fixtures/sproutcore-each-proxy.js +218 -0
- data/spec/fixtures/sproutcore-native-array.js +139 -0
- data/spec/fixtures/sproutcore.js +14319 -0
- data/spec/scope_spec.rb +111 -0
- data/spec/stringify_spec.rb +587 -0
- data/test.rb +76 -0
- metadata +145 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
require "parsejs/visitor"
|
2
|
+
|
3
|
+
module ParseJS
|
4
|
+
module AST
|
5
|
+
module Scope
|
6
|
+
attr_accessor :scope_variables, :parent_variables, :variable_access
|
7
|
+
attr_accessor :parent_scope, :child_scopes
|
8
|
+
|
9
|
+
def variable_in_scope?(name)
|
10
|
+
return true if variable?(name)
|
11
|
+
parent_scope && parent_scope.variable_in_scope?(name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def variable?(name)
|
15
|
+
scope_variable?(name) || parent_variable?(name) || variable_access?(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def scope_variable?(name)
|
19
|
+
scope_variables && scope_variables.include?(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def parent_variable?(name)
|
23
|
+
parent_variables && parent_variables.include?(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def variable_access?(name)
|
27
|
+
variable_access && variable_access.include?(name)
|
28
|
+
end
|
29
|
+
|
30
|
+
# determine whether there is a reference for a particular variable
|
31
|
+
# to a parent scope.
|
32
|
+
def parent_variable_access?(name)
|
33
|
+
# if this scope has a "var x = 1" type of declaration for this name,
|
34
|
+
# it is not referencing a parent scope.
|
35
|
+
return false if scope_variable?(name)
|
36
|
+
|
37
|
+
# otherwise, if there is a variable access for this name, it's
|
38
|
+
# referencing a parent scope.
|
39
|
+
variable_access?(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
# determine whether a variable can be added without causing damage
|
43
|
+
# to child scopes.
|
44
|
+
def available_variable?(name)
|
45
|
+
# if the current scope is already using the variable, it's
|
46
|
+
# unavailable.
|
47
|
+
return false if variable?(name)
|
48
|
+
|
49
|
+
# if any of the child scopes reference the variable as a
|
50
|
+
# parent variable, it's not available.
|
51
|
+
!any_child_references_parent_variable?(name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def any_child_references_parent_variable?(name)
|
55
|
+
unless child_scopes.nil?
|
56
|
+
# this should really check if all descendent scopes see a
|
57
|
+
# scope variable before they see a parent refernce
|
58
|
+
|
59
|
+
return false if child_scopes.all? { |s| s.scope_variable?(name) }
|
60
|
+
|
61
|
+
child_scopes.any? do |s|
|
62
|
+
s.parent_variable_access?(name) ||
|
63
|
+
s.parent_variable?(name) ||
|
64
|
+
s.any_child_references_parent_variable?(name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ProcessVariables < Visitor
|
71
|
+
include ParseJS::Visitor::ScopeManager
|
72
|
+
|
73
|
+
def self.process(ast)
|
74
|
+
new.visit(ast)
|
75
|
+
end
|
76
|
+
|
77
|
+
def push_scope_variable(name)
|
78
|
+
vars = current_scope.scope_variables ||= Set.new
|
79
|
+
vars << name
|
80
|
+
end
|
81
|
+
|
82
|
+
def push_parent_variable(name)
|
83
|
+
vars = current_scope.parent_variables ||= Set.new
|
84
|
+
vars << name
|
85
|
+
end
|
86
|
+
|
87
|
+
def push_variable_access(name)
|
88
|
+
vars = current_scope.variable_access ||= Set.new
|
89
|
+
vars << name
|
90
|
+
end
|
91
|
+
|
92
|
+
def possible_variable_access(var)
|
93
|
+
push_variable_access var.val if var && var.type?("Identifier")
|
94
|
+
end
|
95
|
+
|
96
|
+
def possible_variable_access_array(arr)
|
97
|
+
arr.each { |arg| possible_variable_access arg }
|
98
|
+
end
|
99
|
+
|
100
|
+
def visit_VariableDeclarator(decl)
|
101
|
+
push_scope_variable decl.id.val
|
102
|
+
possible_variable_access decl.init
|
103
|
+
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
def visit_MemberExpression(expr)
|
108
|
+
possible_variable_access expr.object
|
109
|
+
|
110
|
+
super
|
111
|
+
end
|
112
|
+
|
113
|
+
def visit_CallExpression(expr)
|
114
|
+
possible_variable_access expr.callee
|
115
|
+
possible_variable_access_array expr.args
|
116
|
+
|
117
|
+
super
|
118
|
+
end
|
119
|
+
|
120
|
+
def visit_NewExpression(expr)
|
121
|
+
possible_variable_access expr.callee
|
122
|
+
possible_variable_access_array expr.args if expr.args
|
123
|
+
|
124
|
+
super
|
125
|
+
end
|
126
|
+
|
127
|
+
def visit_ArrayExpression(expr)
|
128
|
+
possible_variable_access_array expr.elements
|
129
|
+
|
130
|
+
super
|
131
|
+
end
|
132
|
+
|
133
|
+
def visit_AssignmentExpression(expr)
|
134
|
+
if expr.left.type?("Identifier")
|
135
|
+
push_parent_variable expr.left.val
|
136
|
+
end
|
137
|
+
|
138
|
+
possible_variable_access expr.right
|
139
|
+
|
140
|
+
super
|
141
|
+
end
|
142
|
+
|
143
|
+
def visit_Property(prop)
|
144
|
+
possible_variable_access prop.value
|
145
|
+
|
146
|
+
super
|
147
|
+
end
|
148
|
+
|
149
|
+
def visit_SequenceExpression(expr)
|
150
|
+
possible_variable_access_array expr.expressions
|
151
|
+
|
152
|
+
super
|
153
|
+
end
|
154
|
+
|
155
|
+
def visit_UpdateExpression(expr)
|
156
|
+
possible_variable_access expr.argument
|
157
|
+
|
158
|
+
super
|
159
|
+
end
|
160
|
+
|
161
|
+
def visit_UnaryExpression(expr)
|
162
|
+
possible_variable_access expr.argument
|
163
|
+
|
164
|
+
super
|
165
|
+
end
|
166
|
+
|
167
|
+
def visit_BinaryExpression(expr)
|
168
|
+
possible_variable_access expr.left
|
169
|
+
possible_variable_access expr.right
|
170
|
+
|
171
|
+
super
|
172
|
+
end
|
173
|
+
|
174
|
+
def visit_ConditionalExpression(expr)
|
175
|
+
possible_variable_access expr.alternate
|
176
|
+
possible_variable_access expr.consequent
|
177
|
+
possible_variable_access expr.test
|
178
|
+
|
179
|
+
super
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,450 @@
|
|
1
|
+
require "parsejs/visitor"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module ParseJS
|
5
|
+
require "pp"
|
6
|
+
|
7
|
+
class Stringifier < Visitor
|
8
|
+
def self.to_string(ast)
|
9
|
+
stringifier = new(ast)
|
10
|
+
yield stringifier if block_given?
|
11
|
+
stringifier.to_string
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :include_comments
|
15
|
+
|
16
|
+
def initialize(ast)
|
17
|
+
@ast = ast
|
18
|
+
@indent = 0
|
19
|
+
@include_comments = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def accept(node)
|
23
|
+
return "" if node.nil?
|
24
|
+
|
25
|
+
nl = @newline
|
26
|
+
|
27
|
+
out = if node.cuddly?
|
28
|
+
" " << super
|
29
|
+
elsif @newline
|
30
|
+
@newline = false
|
31
|
+
current_indent << super
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
out << newline if node.needs_newline? && needs_newline?(out)
|
37
|
+
|
38
|
+
out
|
39
|
+
end
|
40
|
+
|
41
|
+
def indent
|
42
|
+
@indent += 1
|
43
|
+
end
|
44
|
+
|
45
|
+
def outdent
|
46
|
+
@indent -= 1
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_indent
|
50
|
+
" " * @indent
|
51
|
+
end
|
52
|
+
|
53
|
+
def newline
|
54
|
+
@newline = true
|
55
|
+
"\n"
|
56
|
+
end
|
57
|
+
|
58
|
+
def cuddle(node, out, more=false)
|
59
|
+
if node.cuddly?
|
60
|
+
node.cuddle! if more
|
61
|
+
else
|
62
|
+
indent
|
63
|
+
out << newline
|
64
|
+
end
|
65
|
+
|
66
|
+
out << accept(node)
|
67
|
+
|
68
|
+
if more && node.cuddly?
|
69
|
+
out << " "
|
70
|
+
@newline = false
|
71
|
+
end
|
72
|
+
|
73
|
+
unless node.cuddly?
|
74
|
+
outdent
|
75
|
+
out << current_indent if more
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def without_newline
|
80
|
+
old, @skip_newline = @skip_newline, true
|
81
|
+
ret = yield
|
82
|
+
@skip_newline = old
|
83
|
+
ret
|
84
|
+
end
|
85
|
+
|
86
|
+
def needs_newline?(out)
|
87
|
+
out !~ /\n$/ && !@skip_newline
|
88
|
+
end
|
89
|
+
|
90
|
+
def strip_newline(str)
|
91
|
+
@newline = false
|
92
|
+
str.sub(/\n$/, '')
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_string
|
96
|
+
accept @ast
|
97
|
+
end
|
98
|
+
|
99
|
+
def params(node)
|
100
|
+
map(node).join(", ")
|
101
|
+
end
|
102
|
+
|
103
|
+
def each(node)
|
104
|
+
node.each { |element| accept(element) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def visit_Program(program)
|
108
|
+
map(program.elements).join("")
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_ExpressionStatement(statement)
|
112
|
+
accept(statement.expression) + ";"
|
113
|
+
end
|
114
|
+
|
115
|
+
def visit_SequenceExpression(expression)
|
116
|
+
out = ""
|
117
|
+
out << "(" if expression.parens
|
118
|
+
exprs = params(expression.expressions)
|
119
|
+
exprs = strip_newline(exprs) if expression.parens
|
120
|
+
out << exprs
|
121
|
+
out << ")" if expression.parens
|
122
|
+
out
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_Literal(literal)
|
126
|
+
case val = literal.val
|
127
|
+
when nil
|
128
|
+
"null"
|
129
|
+
when ParseJS::AST::Node
|
130
|
+
accept val
|
131
|
+
else
|
132
|
+
val.inspect
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def visit_String(string)
|
137
|
+
string.quote + super + string.quote
|
138
|
+
end
|
139
|
+
|
140
|
+
def visit_RegExp(regex)
|
141
|
+
"/#{super.join("/")}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def visit_DebuggerStatement(expr)
|
145
|
+
"#{super};"
|
146
|
+
end
|
147
|
+
|
148
|
+
def visit_UnaryExpression(unary)
|
149
|
+
op, argument = super
|
150
|
+
space = op =~ /\w/ ? sp : ""
|
151
|
+
"#{op}#{space}#{argument}"
|
152
|
+
end
|
153
|
+
|
154
|
+
def visit_AssignmentExpression(expr)
|
155
|
+
left, op, right = super
|
156
|
+
"#{left} #{op} #{right}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def visit_CallExpression(expr)
|
160
|
+
out = strip_newline(accept(expr.callee))
|
161
|
+
args = params(expr.args)
|
162
|
+
args = strip_newline(args)
|
163
|
+
out << "(" + args + ")"
|
164
|
+
end
|
165
|
+
|
166
|
+
def visit_ArrayExpression(expr)
|
167
|
+
|
168
|
+
"[" + begin
|
169
|
+
|
170
|
+
last, out = expr.elements.size - 1, ""
|
171
|
+
expr.elements.each_with_index do |element, i|
|
172
|
+
if element.nil?
|
173
|
+
out << ","
|
174
|
+
out << " " unless i == last
|
175
|
+
else
|
176
|
+
out << accept(element)
|
177
|
+
out << ", " unless i == last
|
178
|
+
end
|
179
|
+
end
|
180
|
+
out
|
181
|
+
|
182
|
+
end + "]"
|
183
|
+
end
|
184
|
+
|
185
|
+
def visit_ObjectExpression(expr)
|
186
|
+
if expr.properties.length > 2
|
187
|
+
out = "{" << newline
|
188
|
+
indent
|
189
|
+
|
190
|
+
last = expr.properties.size - 1
|
191
|
+
expr.properties.each_with_index do |prop, i|
|
192
|
+
out << strip_newline(accept(prop))
|
193
|
+
out << "," unless last == i
|
194
|
+
out << newline
|
195
|
+
end
|
196
|
+
|
197
|
+
outdent
|
198
|
+
out << current_indent << "}"
|
199
|
+
else
|
200
|
+
"{#{params(expr.properties)}}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def visit_Property(property)
|
205
|
+
comments, key, value = super
|
206
|
+
|
207
|
+
"#{comments.join}#{key}: #{value}"
|
208
|
+
end
|
209
|
+
|
210
|
+
def visit_CommentedStatement(statement)
|
211
|
+
comments, statement = super
|
212
|
+
"#{comments.join}#{statement}"
|
213
|
+
end
|
214
|
+
|
215
|
+
def visit_MemberExpression(expr)
|
216
|
+
left = strip_newline(accept(expr.object))
|
217
|
+
right = accept(expr.property)
|
218
|
+
|
219
|
+
if expr.computed
|
220
|
+
"#{left}[#{right}]"
|
221
|
+
else
|
222
|
+
"#{left}.#{right}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def visit_NewExpression(expr)
|
227
|
+
callee, args = super
|
228
|
+
|
229
|
+
left = "new #{callee}"
|
230
|
+
arg_string = "(#{args.join(", ")})" if args
|
231
|
+
return "#{left}#{arg_string}"
|
232
|
+
end
|
233
|
+
|
234
|
+
def visit_BinaryExpression(expr)
|
235
|
+
left = strip_newline(accept(expr.left))
|
236
|
+
right = accept(expr.right)
|
237
|
+
|
238
|
+
"#{left} #{expr.op} #{right}"
|
239
|
+
end
|
240
|
+
|
241
|
+
def visit_BlockStatement(statement)
|
242
|
+
out = "{" << newline
|
243
|
+
indent
|
244
|
+
out << super.join
|
245
|
+
outdent
|
246
|
+
out << current_indent << "}"
|
247
|
+
@newline = false unless statement.cuddly
|
248
|
+
out
|
249
|
+
end
|
250
|
+
|
251
|
+
def visit_IfStatement(statement)
|
252
|
+
consequent = statement.consequent
|
253
|
+
alternate = statement.alternate
|
254
|
+
|
255
|
+
out = "if (" + accept(statement.test) + ")"
|
256
|
+
|
257
|
+
cuddle(consequent, out, alternate)
|
258
|
+
|
259
|
+
if alternate
|
260
|
+
out << "else"
|
261
|
+
cuddle(alternate, out, false)
|
262
|
+
end
|
263
|
+
|
264
|
+
out
|
265
|
+
end
|
266
|
+
|
267
|
+
def visit_WhileStatement(statement)
|
268
|
+
test = statement.test
|
269
|
+
body = statement.body
|
270
|
+
|
271
|
+
out = "while (" + accept(test) + ")"
|
272
|
+
cuddle(body, out, false)
|
273
|
+
out
|
274
|
+
end
|
275
|
+
|
276
|
+
def visit_DoWhileStatement(statement)
|
277
|
+
out = "do"
|
278
|
+
cuddle(statement.body, out, true)
|
279
|
+
out << "while (" + accept(statement.test) + ");"
|
280
|
+
end
|
281
|
+
|
282
|
+
def visit_ForStatement(statement)
|
283
|
+
init = statement.init
|
284
|
+
test = statement.test
|
285
|
+
update = statement.update
|
286
|
+
body = statement.body
|
287
|
+
out = ""
|
288
|
+
|
289
|
+
without_newline do
|
290
|
+
out << "for (" + accept(init) + ";"
|
291
|
+
test = accept(test)
|
292
|
+
out << " #{test}" unless test.empty?
|
293
|
+
out << ";"
|
294
|
+
update = accept(update)
|
295
|
+
out << " #{update}" unless update.empty?
|
296
|
+
out << ")"
|
297
|
+
end
|
298
|
+
|
299
|
+
cuddle(body, out, false)
|
300
|
+
out
|
301
|
+
end
|
302
|
+
|
303
|
+
def visit_VariableDeclaration(decl)
|
304
|
+
kind, declarations, semicolon = super
|
305
|
+
"#{kind} #{declarations.join(", ")}#{";" if semicolon}"
|
306
|
+
end
|
307
|
+
|
308
|
+
def visit_VariableDeclarator(decl)
|
309
|
+
id, init = super
|
310
|
+
|
311
|
+
out = id
|
312
|
+
out << (init ? " = #{init}" : "")
|
313
|
+
end
|
314
|
+
|
315
|
+
def visit_UpdateExpression(expr)
|
316
|
+
op, prefix, argument = super
|
317
|
+
|
318
|
+
op += " " if op =~ /\w/
|
319
|
+
|
320
|
+
if prefix
|
321
|
+
"#{op}#{argument}"
|
322
|
+
else
|
323
|
+
"#{argument}#{op}"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def visit_ForInStatement(statement)
|
328
|
+
left = statement.left
|
329
|
+
right = statement.right
|
330
|
+
body = statement.body
|
331
|
+
|
332
|
+
out = ""
|
333
|
+
|
334
|
+
without_newline do
|
335
|
+
out << "for (" + accept(left) + " in " + accept(right) + ")"
|
336
|
+
end
|
337
|
+
|
338
|
+
cuddle(body, out, false)
|
339
|
+
out
|
340
|
+
end
|
341
|
+
|
342
|
+
def visit_SwitchStatement(statement)
|
343
|
+
out = ""
|
344
|
+
|
345
|
+
without_newline do
|
346
|
+
out << "switch (" + accept(statement.discriminant) + ") {" << newline
|
347
|
+
indent
|
348
|
+
end
|
349
|
+
|
350
|
+
out << map(statement.cases).join
|
351
|
+
outdent
|
352
|
+
out << current_indent << "}" << newline
|
353
|
+
end
|
354
|
+
|
355
|
+
def visit_SwitchCase(switch)
|
356
|
+
if switch.test
|
357
|
+
out = "case #{accept(switch.test)}:" << newline
|
358
|
+
else
|
359
|
+
out = "default:" << newline
|
360
|
+
end
|
361
|
+
|
362
|
+
indent
|
363
|
+
out << map(switch.consequent).join
|
364
|
+
outdent
|
365
|
+
out
|
366
|
+
end
|
367
|
+
|
368
|
+
def visit_ThrowStatement(statement)
|
369
|
+
"throw #{super};"
|
370
|
+
end
|
371
|
+
|
372
|
+
def visit_TryStatement(statement)
|
373
|
+
handler = statement.handler
|
374
|
+
finalizer = statement.finalizer
|
375
|
+
|
376
|
+
out = "try"
|
377
|
+
|
378
|
+
cuddle(statement.block, out, handler || finalizer)
|
379
|
+
|
380
|
+
if handler
|
381
|
+
out << "catch (" + accept(handler.param) + ")"
|
382
|
+
cuddle(handler.body, out, finalizer)
|
383
|
+
end
|
384
|
+
|
385
|
+
if finalizer
|
386
|
+
out << "finally"
|
387
|
+
cuddle(finalizer, out, false)
|
388
|
+
end
|
389
|
+
|
390
|
+
out
|
391
|
+
end
|
392
|
+
|
393
|
+
def visit_FunctionDeclaration(decl)
|
394
|
+
id = decl.id
|
395
|
+
parameters = decl.params.list
|
396
|
+
body = decl.body
|
397
|
+
|
398
|
+
out = "function " + accept(id) + "("
|
399
|
+
out << params(parameters)
|
400
|
+
out << ") {" << newline
|
401
|
+
|
402
|
+
indent
|
403
|
+
out << map(decl.body).join
|
404
|
+
outdent
|
405
|
+
out << current_indent << "}"
|
406
|
+
end
|
407
|
+
|
408
|
+
def labeled(name, label)
|
409
|
+
out = name
|
410
|
+
out << " #{label}" if label
|
411
|
+
out << ";"
|
412
|
+
end
|
413
|
+
|
414
|
+
def visit_ReturnStatement(statement)
|
415
|
+
labeled("return", super)
|
416
|
+
end
|
417
|
+
|
418
|
+
def visit_BreakStatement(statement)
|
419
|
+
labeled("break", super)
|
420
|
+
end
|
421
|
+
|
422
|
+
def visit_ContinueStatement(statement)
|
423
|
+
labeled("continue", super)
|
424
|
+
end
|
425
|
+
|
426
|
+
def visit_ConditionalExpression(expr)
|
427
|
+
out = strip_newline(accept(expr.test))
|
428
|
+
out << " ? " << strip_newline(accept(expr.consequent))
|
429
|
+
out << " : " << accept(expr.alternate)
|
430
|
+
end
|
431
|
+
|
432
|
+
def visit_Comment(comment)
|
433
|
+
return "" unless include_comments
|
434
|
+
if comment.type == 'singleline'
|
435
|
+
"//" + comment.body + newline
|
436
|
+
else
|
437
|
+
body = comment.body.split("\n")
|
438
|
+
first = body.shift
|
439
|
+
out = "/*" + first + newline + body.map { |s| "#{current_indent}#{s}" }.join(newline) + "*/"
|
440
|
+
out << "\n" if comment.newline
|
441
|
+
out
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
private
|
446
|
+
def sp
|
447
|
+
" "
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|