racc 1.4.6
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.
- data/.gitattributes +2 -0
- data/.gitignore +7 -0
- data/COPYING +515 -0
- data/ChangeLog +846 -0
- data/DEPENDS +4 -0
- data/README.en.rdoc +86 -0
- data/README.ja.rdoc +96 -0
- data/Rakefile +15 -0
- data/TODO +5 -0
- data/bin/racc +308 -0
- data/bin/racc2y +195 -0
- data/bin/y2racc +339 -0
- data/doc/en/NEWS.en.rdoc +282 -0
- data/doc/en/command.en.html +78 -0
- data/doc/en/debug.en.rdoc +20 -0
- data/doc/en/grammar.en.rdoc +230 -0
- data/doc/en/index.en.html +10 -0
- data/doc/en/parser.en.rdoc +74 -0
- data/doc/en/usage.en.html +92 -0
- data/doc/ja/NEWS.ja.rdoc +307 -0
- data/doc/ja/command.ja.html +94 -0
- data/doc/ja/debug.ja.rdoc +36 -0
- data/doc/ja/grammar.ja.rdoc +348 -0
- data/doc/ja/index.ja.html +10 -0
- data/doc/ja/parser.ja.rdoc +125 -0
- data/doc/ja/usage.ja.html +414 -0
- data/ext/racc/cparse/MANIFEST +4 -0
- data/ext/racc/cparse/cparse.c +824 -0
- data/ext/racc/cparse/depend +1 -0
- data/ext/racc/cparse/extconf.rb +7 -0
- data/fastcache/extconf.rb +2 -0
- data/fastcache/fastcache.c +185 -0
- data/lib/racc.rb +6 -0
- data/lib/racc/compat.rb +40 -0
- data/lib/racc/debugflags.rb +59 -0
- data/lib/racc/exception.rb +15 -0
- data/lib/racc/grammar.rb +1115 -0
- data/lib/racc/grammarfileparser.rb +559 -0
- data/lib/racc/info.rb +16 -0
- data/lib/racc/iset.rb +91 -0
- data/lib/racc/logfilegenerator.rb +214 -0
- data/lib/racc/parser.rb +439 -0
- data/lib/racc/parserfilegenerator.rb +511 -0
- data/lib/racc/pre-setup +13 -0
- data/lib/racc/sourcetext.rb +34 -0
- data/lib/racc/state.rb +971 -0
- data/lib/racc/statetransitiontable.rb +316 -0
- data/lib/racc/static.rb +5 -0
- data/misc/dist.sh +31 -0
- data/sample/array.y +67 -0
- data/sample/array2.y +59 -0
- data/sample/calc-ja.y +66 -0
- data/sample/calc.y +65 -0
- data/sample/conflict.y +15 -0
- data/sample/hash.y +60 -0
- data/sample/lalr.y +17 -0
- data/sample/lists.y +57 -0
- data/sample/syntax.y +46 -0
- data/sample/yyerr.y +46 -0
- data/setup.rb +1587 -0
- data/tasks/doc.rb +12 -0
- data/tasks/email.rb +55 -0
- data/tasks/file.rb +37 -0
- data/tasks/gem.rb +37 -0
- data/tasks/test.rb +16 -0
- data/test/assets/chk.y +126 -0
- data/test/assets/conf.y +16 -0
- data/test/assets/digraph.y +29 -0
- data/test/assets/echk.y +118 -0
- data/test/assets/err.y +60 -0
- data/test/assets/expect.y +7 -0
- data/test/assets/firstline.y +4 -0
- data/test/assets/ichk.y +102 -0
- data/test/assets/intp.y +546 -0
- data/test/assets/mailp.y +437 -0
- data/test/assets/newsyn.y +25 -0
- data/test/assets/noend.y +4 -0
- data/test/assets/nonass.y +41 -0
- data/test/assets/normal.y +27 -0
- data/test/assets/norule.y +4 -0
- data/test/assets/nullbug1.y +25 -0
- data/test/assets/nullbug2.y +15 -0
- data/test/assets/opt.y +123 -0
- data/test/assets/percent.y +35 -0
- data/test/assets/recv.y +97 -0
- data/test/assets/rrconf.y +14 -0
- data/test/assets/scan.y +72 -0
- data/test/assets/syntax.y +50 -0
- data/test/assets/unterm.y +5 -0
- data/test/assets/useless.y +12 -0
- data/test/assets/yyerr.y +46 -0
- data/test/bench.y +36 -0
- data/test/helper.rb +88 -0
- data/test/infini.y +8 -0
- data/test/scandata/brace +7 -0
- data/test/scandata/gvar +1 -0
- data/test/scandata/normal +4 -0
- data/test/scandata/percent +18 -0
- data/test/scandata/slash +10 -0
- data/test/src.intp +34 -0
- data/test/start.y +20 -0
- data/test/test_chk_y.rb +51 -0
- data/test/test_grammar_file_parser.rb +15 -0
- data/test/test_racc_command.rb +155 -0
- data/test/test_scan_y.rb +51 -0
- data/test/testscanner.rb +51 -0
- data/web/racc.en.rhtml +42 -0
- data/web/racc.ja.rhtml +51 -0
- metadata +166 -0
@@ -0,0 +1,511 @@
|
|
1
|
+
#
|
2
|
+
# $Id$
|
3
|
+
#
|
4
|
+
# Copyright (c) 1999-2006 Minero Aoki
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU LGPL, Lesser General Public License version 2.1.
|
9
|
+
# For details of the GNU LGPL, see the file "COPYING".
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'enumerator'
|
13
|
+
require 'racc/compat'
|
14
|
+
require 'racc/sourcetext'
|
15
|
+
require 'racc/parser-text'
|
16
|
+
require 'rbconfig'
|
17
|
+
|
18
|
+
module Racc
|
19
|
+
|
20
|
+
class ParserFileGenerator
|
21
|
+
|
22
|
+
class Params
|
23
|
+
def self.bool_attr(name)
|
24
|
+
module_eval(<<-End)
|
25
|
+
def #{name}?
|
26
|
+
@#{name}
|
27
|
+
end
|
28
|
+
|
29
|
+
def #{name}=(b)
|
30
|
+
@#{name} = b
|
31
|
+
end
|
32
|
+
End
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_accessor :filename
|
36
|
+
attr_accessor :classname
|
37
|
+
attr_accessor :superclass
|
38
|
+
bool_attr :omit_action_call
|
39
|
+
bool_attr :result_var
|
40
|
+
attr_accessor :header
|
41
|
+
attr_accessor :inner
|
42
|
+
attr_accessor :footer
|
43
|
+
|
44
|
+
bool_attr :debug_parser
|
45
|
+
bool_attr :convert_line
|
46
|
+
bool_attr :convert_line_all
|
47
|
+
bool_attr :embed_runtime
|
48
|
+
bool_attr :make_executable
|
49
|
+
attr_accessor :interpreter
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
# Parameters derived from parser
|
53
|
+
self.filename = nil
|
54
|
+
self.classname = nil
|
55
|
+
self.superclass = 'Racc::Parser'
|
56
|
+
self.omit_action_call = true
|
57
|
+
self.result_var = true
|
58
|
+
self.header = []
|
59
|
+
self.inner = []
|
60
|
+
self.footer = []
|
61
|
+
|
62
|
+
# Parameters derived from command line options
|
63
|
+
self.debug_parser = false
|
64
|
+
self.convert_line = true
|
65
|
+
self.convert_line_all = false
|
66
|
+
self.embed_runtime = false
|
67
|
+
self.make_executable = false
|
68
|
+
self.interpreter = nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(states, params)
|
73
|
+
@states = states
|
74
|
+
@grammar = states.grammar
|
75
|
+
@params = params
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_parser
|
79
|
+
string_io = StringIO.new
|
80
|
+
|
81
|
+
init_line_conversion_system
|
82
|
+
@f = string_io
|
83
|
+
parser_file
|
84
|
+
|
85
|
+
string_io.rewind
|
86
|
+
string_io.read
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_parser_file(destpath)
|
90
|
+
init_line_conversion_system
|
91
|
+
File.open(destpath, 'w') {|f|
|
92
|
+
@f = f
|
93
|
+
parser_file
|
94
|
+
}
|
95
|
+
File.chmod 0755, destpath if @params.make_executable?
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def parser_file
|
101
|
+
shebang @params.interpreter if @params.make_executable?
|
102
|
+
notice
|
103
|
+
line
|
104
|
+
if @params.embed_runtime?
|
105
|
+
embed_library runtime_source()
|
106
|
+
else
|
107
|
+
require 'racc/parser.rb'
|
108
|
+
end
|
109
|
+
header
|
110
|
+
parser_class(@params.classname, @params.superclass) {
|
111
|
+
inner
|
112
|
+
state_transition_table
|
113
|
+
}
|
114
|
+
footer
|
115
|
+
end
|
116
|
+
|
117
|
+
c = ::Config::CONFIG
|
118
|
+
RUBY_PATH = "#{c['bindir']}/#{c['ruby_install_name']}#{c['EXEEXT']}"
|
119
|
+
|
120
|
+
def shebang(path)
|
121
|
+
line '#!' + (path == 'ruby' ? RUBY_PATH : path)
|
122
|
+
end
|
123
|
+
|
124
|
+
def notice
|
125
|
+
line %q[#]
|
126
|
+
line %q[# DO NOT MODIFY!!!!]
|
127
|
+
line %Q[# This file is automatically generated by Racc #{Racc::Version}]
|
128
|
+
line %Q[# from Racc grammer file "#{@params.filename}".]
|
129
|
+
line %q[#]
|
130
|
+
end
|
131
|
+
|
132
|
+
def runtime_source
|
133
|
+
SourceText.new(::Racc::PARSER_TEXT, 'racc/parser.rb', 1)
|
134
|
+
end
|
135
|
+
|
136
|
+
def embed_library(src)
|
137
|
+
line %[###### #{src.filename} begin]
|
138
|
+
line %[unless $".index '#{src.filename}']
|
139
|
+
line %[$".push '#{src.filename}']
|
140
|
+
put src, @params.convert_line?
|
141
|
+
line %[end]
|
142
|
+
line %[###### #{src.filename} end]
|
143
|
+
end
|
144
|
+
|
145
|
+
def require(feature)
|
146
|
+
line "require '#{feature}'"
|
147
|
+
end
|
148
|
+
|
149
|
+
def parser_class(classname, superclass)
|
150
|
+
mods = classname.split('::')
|
151
|
+
classid = mods.pop
|
152
|
+
mods.each do |mod|
|
153
|
+
indent; line "module #{mod}"
|
154
|
+
cref_push mod
|
155
|
+
end
|
156
|
+
indent; line "class #{classid} < #{superclass}"
|
157
|
+
cref_push classid
|
158
|
+
yield
|
159
|
+
cref_pop
|
160
|
+
indent; line "end \# class #{classid}"
|
161
|
+
mods.reverse_each do |mod|
|
162
|
+
indent; line "end \# module #{mod}"
|
163
|
+
cref_pop
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def header
|
168
|
+
@params.header.each do |src|
|
169
|
+
line
|
170
|
+
put src, @params.convert_line_all?
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def inner
|
175
|
+
@params.inner.each do |src|
|
176
|
+
line
|
177
|
+
put src, @params.convert_line?
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def footer
|
182
|
+
@params.footer.each do |src|
|
183
|
+
line
|
184
|
+
put src, @params.convert_line_all?
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Low Level Routines
|
189
|
+
|
190
|
+
def put(src, convert_line = false)
|
191
|
+
if convert_line
|
192
|
+
replace_location(src) {
|
193
|
+
@f.puts src.text
|
194
|
+
}
|
195
|
+
else
|
196
|
+
@f.puts src.text
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def line(str = '')
|
201
|
+
@f.puts str
|
202
|
+
end
|
203
|
+
|
204
|
+
def init_line_conversion_system
|
205
|
+
@cref = []
|
206
|
+
@used_separator = {}
|
207
|
+
end
|
208
|
+
|
209
|
+
def cref_push(name)
|
210
|
+
@cref.push name
|
211
|
+
end
|
212
|
+
|
213
|
+
def cref_pop
|
214
|
+
@cref.pop
|
215
|
+
end
|
216
|
+
|
217
|
+
def indent
|
218
|
+
@f.print ' ' * @cref.size
|
219
|
+
end
|
220
|
+
|
221
|
+
def toplevel?
|
222
|
+
@cref.empty?
|
223
|
+
end
|
224
|
+
|
225
|
+
def replace_location(src)
|
226
|
+
sep = make_separator(src)
|
227
|
+
@f.print 'self.class.' if toplevel?
|
228
|
+
@f.puts "module_eval(<<'#{sep}', '#{src.filename}', #{src.lineno})"
|
229
|
+
yield
|
230
|
+
@f.puts sep
|
231
|
+
end
|
232
|
+
|
233
|
+
def make_separator(src)
|
234
|
+
sep = unique_separator(src.filename)
|
235
|
+
sep *= 2 while src.text.index(sep)
|
236
|
+
sep
|
237
|
+
end
|
238
|
+
|
239
|
+
def unique_separator(id)
|
240
|
+
sep = "...end #{id}/module_eval..."
|
241
|
+
while @used_separator.key?(sep)
|
242
|
+
sep.concat sprintf('%02x', rand(255))
|
243
|
+
end
|
244
|
+
@used_separator[sep] = true
|
245
|
+
sep
|
246
|
+
end
|
247
|
+
|
248
|
+
#
|
249
|
+
# State Transition Table Serialization
|
250
|
+
#
|
251
|
+
|
252
|
+
public
|
253
|
+
|
254
|
+
def put_state_transition_table(f)
|
255
|
+
@f = f
|
256
|
+
state_transition_table
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
def state_transition_table
|
262
|
+
table = @states.state_transition_table
|
263
|
+
table.use_result_var = @params.result_var?
|
264
|
+
table.debug_parser = @params.debug_parser?
|
265
|
+
|
266
|
+
line "##### State transition tables begin ###"
|
267
|
+
line
|
268
|
+
integer_list 'racc_action_table', table.action_table
|
269
|
+
line
|
270
|
+
integer_list 'racc_action_check', table.action_check
|
271
|
+
line
|
272
|
+
integer_list 'racc_action_pointer', table.action_pointer
|
273
|
+
line
|
274
|
+
integer_list 'racc_action_default', table.action_default
|
275
|
+
line
|
276
|
+
integer_list 'racc_goto_table', table.goto_table
|
277
|
+
line
|
278
|
+
integer_list 'racc_goto_check', table.goto_check
|
279
|
+
line
|
280
|
+
integer_list 'racc_goto_pointer', table.goto_pointer
|
281
|
+
line
|
282
|
+
integer_list 'racc_goto_default', table.goto_default
|
283
|
+
line
|
284
|
+
i_i_sym_list 'racc_reduce_table', table.reduce_table
|
285
|
+
line
|
286
|
+
line "racc_reduce_n = #{table.reduce_n}"
|
287
|
+
line
|
288
|
+
line "racc_shift_n = #{table.shift_n}"
|
289
|
+
line
|
290
|
+
sym_int_hash 'racc_token_table', table.token_table
|
291
|
+
line
|
292
|
+
line "racc_nt_base = #{table.nt_base}"
|
293
|
+
line
|
294
|
+
line "racc_use_result_var = #{table.use_result_var}"
|
295
|
+
line
|
296
|
+
@f.print(unindent_auto(<<-End))
|
297
|
+
Racc_arg = [
|
298
|
+
racc_action_table,
|
299
|
+
racc_action_check,
|
300
|
+
racc_action_default,
|
301
|
+
racc_action_pointer,
|
302
|
+
racc_goto_table,
|
303
|
+
racc_goto_check,
|
304
|
+
racc_goto_default,
|
305
|
+
racc_goto_pointer,
|
306
|
+
racc_nt_base,
|
307
|
+
racc_reduce_table,
|
308
|
+
racc_token_table,
|
309
|
+
racc_shift_n,
|
310
|
+
racc_reduce_n,
|
311
|
+
racc_use_result_var ]
|
312
|
+
End
|
313
|
+
line
|
314
|
+
string_list 'Racc_token_to_s_table', table.token_to_s_table
|
315
|
+
line
|
316
|
+
line "Racc_debug_parser = #{table.debug_parser}"
|
317
|
+
line
|
318
|
+
line '##### State transition tables end #####'
|
319
|
+
actions
|
320
|
+
end
|
321
|
+
|
322
|
+
def integer_list(name, table)
|
323
|
+
if table.size > 2000
|
324
|
+
serialize_integer_list_compressed name, table
|
325
|
+
else
|
326
|
+
serialize_integer_list_std name, table
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def serialize_integer_list_compressed(name, table)
|
331
|
+
sep = "\n"
|
332
|
+
nsep = ",\n"
|
333
|
+
buf = ''
|
334
|
+
com = ''
|
335
|
+
ncom = ','
|
336
|
+
co = com
|
337
|
+
@f.print 'clist = ['
|
338
|
+
table.each do |i|
|
339
|
+
buf << co << i.to_s; co = ncom
|
340
|
+
if buf.size > 66
|
341
|
+
@f.print sep; sep = nsep
|
342
|
+
@f.print "'", buf, "'"
|
343
|
+
buf = ''
|
344
|
+
co = com
|
345
|
+
end
|
346
|
+
end
|
347
|
+
unless buf.empty?
|
348
|
+
@f.print sep
|
349
|
+
@f.print "'", buf, "'"
|
350
|
+
end
|
351
|
+
line ' ]'
|
352
|
+
|
353
|
+
@f.print(<<-End)
|
354
|
+
#{name} = arr = Array.new(#{table.size}, nil)
|
355
|
+
idx = 0
|
356
|
+
clist.each do |str|
|
357
|
+
str.split(',', -1).each do |i|
|
358
|
+
arr[idx] = i.to_i unless i.empty?
|
359
|
+
idx += 1
|
360
|
+
end
|
361
|
+
end
|
362
|
+
End
|
363
|
+
end
|
364
|
+
|
365
|
+
def serialize_integer_list_std(name, table)
|
366
|
+
sep = ''
|
367
|
+
line "#{name} = ["
|
368
|
+
table.each_slice(10) do |ns|
|
369
|
+
@f.print sep; sep = ",\n"
|
370
|
+
@f.print ns.map {|n| sprintf('%6s', n ? n.to_s : 'nil') }.join(',')
|
371
|
+
end
|
372
|
+
line ' ]'
|
373
|
+
end
|
374
|
+
|
375
|
+
def i_i_sym_list(name, table)
|
376
|
+
sep = ''
|
377
|
+
line "#{name} = ["
|
378
|
+
table.each_slice(3) do |len, target, mid|
|
379
|
+
@f.print sep; sep = ",\n"
|
380
|
+
@f.printf ' %d, %d, %s', len, target, mid.inspect
|
381
|
+
end
|
382
|
+
line " ]"
|
383
|
+
end
|
384
|
+
|
385
|
+
def sym_int_hash(name, h)
|
386
|
+
sep = "\n"
|
387
|
+
@f.print "#{name} = {"
|
388
|
+
h.to_a.sort_by {|sym, i| i }.each do |sym, i|
|
389
|
+
@f.print sep; sep = ",\n"
|
390
|
+
@f.printf " %s => %d", sym.serialize, i
|
391
|
+
end
|
392
|
+
line " }"
|
393
|
+
end
|
394
|
+
|
395
|
+
def string_list(name, list)
|
396
|
+
sep = " "
|
397
|
+
line "#{name} = ["
|
398
|
+
list.each do |s|
|
399
|
+
@f.print sep; sep = ",\n "
|
400
|
+
@f.print s.dump
|
401
|
+
end
|
402
|
+
line ' ]'
|
403
|
+
end
|
404
|
+
|
405
|
+
def actions
|
406
|
+
@grammar.each do |rule|
|
407
|
+
unless rule.action.source?
|
408
|
+
raise "racc: fatal: cannot generate parser file when any action is a Proc"
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
if @params.result_var?
|
413
|
+
decl = ', result'
|
414
|
+
retval = "\n result"
|
415
|
+
default_body = ''
|
416
|
+
else
|
417
|
+
decl = ''
|
418
|
+
retval = ''
|
419
|
+
default_body = 'val[0]'
|
420
|
+
end
|
421
|
+
@grammar.each do |rule|
|
422
|
+
line
|
423
|
+
if rule.action.empty? and @params.omit_action_call?
|
424
|
+
line "# reduce #{rule.ident} omitted"
|
425
|
+
else
|
426
|
+
src0 = rule.action.source || SourceText.new(default_body, __FILE__, 0)
|
427
|
+
if @params.convert_line?
|
428
|
+
src = remove_blank_lines(src0)
|
429
|
+
delim = make_delimiter(src.text)
|
430
|
+
@f.printf unindent_auto(<<-End),
|
431
|
+
module_eval(<<'%s', '%s', %d)
|
432
|
+
def _reduce_%d(val, _values%s)
|
433
|
+
%s%s
|
434
|
+
end
|
435
|
+
%s
|
436
|
+
End
|
437
|
+
delim, src.filename, src.lineno - 1,
|
438
|
+
rule.ident, decl,
|
439
|
+
src.text, retval,
|
440
|
+
delim
|
441
|
+
else
|
442
|
+
src = remove_blank_lines(src0)
|
443
|
+
@f.printf unindent_auto(<<-End),
|
444
|
+
def _reduce_%d(val, _values%s)
|
445
|
+
%s%s
|
446
|
+
end
|
447
|
+
End
|
448
|
+
rule.ident, decl,
|
449
|
+
src.text, retval
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
line
|
454
|
+
@f.printf unindent_auto(<<-'End'), decl
|
455
|
+
def _reduce_none(val, _values%s)
|
456
|
+
val[0]
|
457
|
+
end
|
458
|
+
End
|
459
|
+
line
|
460
|
+
end
|
461
|
+
|
462
|
+
def remove_blank_lines(src)
|
463
|
+
body = src.text.dup
|
464
|
+
line = src.lineno
|
465
|
+
while m = body.slice!(/\A[ \t\f]*(?:\n|\r\n|\r)/)
|
466
|
+
line += 1
|
467
|
+
end
|
468
|
+
SourceText.new(body, src.filename, line)
|
469
|
+
end
|
470
|
+
|
471
|
+
def make_delimiter(body)
|
472
|
+
delim = '.,.,'
|
473
|
+
while body.index(delim)
|
474
|
+
delim *= 2
|
475
|
+
end
|
476
|
+
delim
|
477
|
+
end
|
478
|
+
|
479
|
+
def unindent_auto(str)
|
480
|
+
lines = str.to_a
|
481
|
+
n = minimum_indent(lines)
|
482
|
+
lines.map {|line| detab(line).sub(indent_re(n), '').rstrip + "\n" }.join('')
|
483
|
+
end
|
484
|
+
|
485
|
+
def minimum_indent(lines)
|
486
|
+
lines.map {|line| n_indent(line) }.min
|
487
|
+
end
|
488
|
+
|
489
|
+
def n_indent(line)
|
490
|
+
line.slice(/\A\s+/).size
|
491
|
+
end
|
492
|
+
|
493
|
+
RE_CACHE = {}
|
494
|
+
|
495
|
+
def indent_re(n)
|
496
|
+
RE_CACHE[n] ||= /\A {#{n}}/
|
497
|
+
end
|
498
|
+
|
499
|
+
def detab(str, ts = 8)
|
500
|
+
add = 0
|
501
|
+
len = nil
|
502
|
+
str.gsub(/\t/) {
|
503
|
+
len = ts - ($`.size + add) % ts
|
504
|
+
add += len - 1
|
505
|
+
' ' * len
|
506
|
+
}
|
507
|
+
end
|
508
|
+
|
509
|
+
end
|
510
|
+
|
511
|
+
end
|