sublime_dsl 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/Rakefile +248 -0
- data/SYNTAX.md +927 -0
- data/bin/subdsl +4 -0
- data/lib/sublime_dsl/cli/export.rb +134 -0
- data/lib/sublime_dsl/cli/import.rb +143 -0
- data/lib/sublime_dsl/cli.rb +125 -0
- data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
- data/lib/sublime_dsl/core_ext/string.rb +129 -0
- data/lib/sublime_dsl/core_ext.rb +4 -0
- data/lib/sublime_dsl/sublime_text/command.rb +157 -0
- data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
- data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
- data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
- data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
- data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
- data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
- data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
- data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
- data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
- data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
- data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
- data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
- data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
- data/lib/sublime_dsl/sublime_text/package.rb +96 -0
- data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
- data/lib/sublime_dsl/sublime_text.rb +48 -0
- data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
- data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
- data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
- data/lib/sublime_dsl/textmate/grammar.rb +252 -0
- data/lib/sublime_dsl/textmate/plist.rb +141 -0
- data/lib/sublime_dsl/textmate/preference.rb +301 -0
- data/lib/sublime_dsl/textmate/snippet.rb +437 -0
- data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
- data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
- data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
- data/lib/sublime_dsl/textmate/theme.rb +364 -0
- data/lib/sublime_dsl/textmate.rb +9 -0
- data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
- data/lib/sublime_dsl/tools/console.rb +74 -0
- data/lib/sublime_dsl/tools/helpers.rb +152 -0
- data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
- data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
- data/lib/sublime_dsl/tools/value_equality.rb +37 -0
- data/lib/sublime_dsl/tools/xml.rb +66 -0
- data/lib/sublime_dsl/tools.rb +66 -0
- data/lib/sublime_dsl.rb +23 -0
- metadata +145 -0
@@ -0,0 +1,364 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'theme/item'
|
4
|
+
require_relative 'theme/dsl_reader'
|
5
|
+
require_relative 'theme/plist_writer'
|
6
|
+
|
7
|
+
module SublimeDSL
|
8
|
+
module TextMate
|
9
|
+
|
10
|
+
class Theme
|
11
|
+
|
12
|
+
# Create from a PList file.
|
13
|
+
def self.import(file)
|
14
|
+
p = PList.import(file)
|
15
|
+
t = new(p.delete('name'))
|
16
|
+
t.basename = File.basename(file, File.extname(file))
|
17
|
+
t.author = p.delete('author')
|
18
|
+
t.uuid = p.delete('uuid')
|
19
|
+
t.comment = p.delete('comment')
|
20
|
+
t.license = p.delete('license')
|
21
|
+
p.delete('settings').each do |h|
|
22
|
+
n = h['name']
|
23
|
+
s = h['scope'] && h['scope'].strip
|
24
|
+
if n || s
|
25
|
+
e = Item.new(n, s)
|
26
|
+
h['settings'].each_pair do |prop, value|
|
27
|
+
next if value.empty?
|
28
|
+
e.send "#{prop.snake_case}=", value
|
29
|
+
end
|
30
|
+
t.add_item e
|
31
|
+
else
|
32
|
+
h['settings'].each_pair do |prop, color|
|
33
|
+
key = prop.snake_case #.to_sym
|
34
|
+
color.upcase! if color =~ /^#/
|
35
|
+
t.base_colors[key] = color
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
p.empty? or raise Error, "unknown theme keys: #{p.inspect}"
|
40
|
+
t
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the content of this file after __END__
|
44
|
+
# (DATA is not available for required files).
|
45
|
+
|
46
|
+
def self.color_data # :nodoc:
|
47
|
+
data = ''
|
48
|
+
File.open(__FILE__) do |f|
|
49
|
+
in_data = false
|
50
|
+
f.each_line do |line|
|
51
|
+
if line =~ /^__END__/
|
52
|
+
in_data = true
|
53
|
+
elsif in_data
|
54
|
+
data << line
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
data
|
59
|
+
end
|
60
|
+
|
61
|
+
# Hash { '#xxxxxx' => color_name }, read from the
|
62
|
+
# DATA section of this file.
|
63
|
+
|
64
|
+
COLOR_NAMES_HASH = {}
|
65
|
+
|
66
|
+
color_data.each_line do |line|
|
67
|
+
line.strip!
|
68
|
+
next if line.empty? || line.start_with?('=')
|
69
|
+
name, value = line.split(/\s+/)
|
70
|
+
COLOR_NAMES_HASH["##{value}"] = name.snake_case
|
71
|
+
end
|
72
|
+
|
73
|
+
include CustomBaseName
|
74
|
+
|
75
|
+
attr_accessor :name, :author, :uuid, :comment, :license
|
76
|
+
|
77
|
+
# base color hash:
|
78
|
+
# background
|
79
|
+
# caret
|
80
|
+
# foreground
|
81
|
+
# invisibles
|
82
|
+
# line_highlight
|
83
|
+
# selection
|
84
|
+
# selection_border
|
85
|
+
# inactive_selection
|
86
|
+
#
|
87
|
+
# Monokai* also have:
|
88
|
+
# inactive_selection
|
89
|
+
# inactive_selection_foreground
|
90
|
+
# find_highlight
|
91
|
+
# find_highlight_foreground
|
92
|
+
# active_guide
|
93
|
+
# brackets_foreground
|
94
|
+
# brackets_options
|
95
|
+
# bracket_contents_foreground
|
96
|
+
# bracket_contents_options
|
97
|
+
|
98
|
+
attr_reader :base_colors
|
99
|
+
|
100
|
+
# array of defined items
|
101
|
+
attr_reader :items
|
102
|
+
|
103
|
+
def initialize(name)
|
104
|
+
@name = name
|
105
|
+
@author = nil
|
106
|
+
@uuid = nil
|
107
|
+
@comment = nil
|
108
|
+
@license = nil
|
109
|
+
@base_colors = {}
|
110
|
+
@items = []
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_item(e)
|
114
|
+
warn "unnamed item in theme #{name.inspect}" unless e.name
|
115
|
+
warn "empty scope for item '#{e.name}' in theme #{name.inspect}" unless e.scope
|
116
|
+
e.name && items.any? { |el| el.name == e.name } and
|
117
|
+
warn "duplicate item name '#{e.name}' in theme #{name.inspect}"
|
118
|
+
items << e
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_dsl
|
122
|
+
out = StringIO.new('', 'wb:utf-8')
|
123
|
+
|
124
|
+
args = name.to_source
|
125
|
+
args << dsl_file_arg
|
126
|
+
|
127
|
+
out.puts comment.strip.lines.map { |l| "# #{l}" }.join << "\n\n" if comment
|
128
|
+
out.puts "theme #{args} do"
|
129
|
+
out.puts
|
130
|
+
out.puts " author '#{author}'" if author
|
131
|
+
out.puts " uuid '#{uuid}'" if uuid
|
132
|
+
if license
|
133
|
+
out.puts
|
134
|
+
out.puts " license <<-TXT"
|
135
|
+
license.lines.each { |l| out.puts l.strip.empty? ? '' : l.strip.wrap.indent(4) }
|
136
|
+
out.puts " TXT"
|
137
|
+
end
|
138
|
+
|
139
|
+
out.puts
|
140
|
+
max = colors_hash.values.map(&:length).max
|
141
|
+
colors_hash.each_pair do |color, name|
|
142
|
+
out.puts " %*1$2$s = '%3$s'" % [-max, name, color]
|
143
|
+
end
|
144
|
+
|
145
|
+
out.puts
|
146
|
+
out.puts " base_colors("
|
147
|
+
base_colors.each_pair do |k,v|
|
148
|
+
out.puts " #{k}: #{colors_hash[v]},"
|
149
|
+
end
|
150
|
+
out.puts " )"
|
151
|
+
|
152
|
+
out.puts
|
153
|
+
names_hash = items.group_by(&:name)
|
154
|
+
items.each do |e|
|
155
|
+
out.puts ' # FIXME: duplicate name:' if e.name && names_hash[e.name].length > 1
|
156
|
+
out.puts e.to_dsl(colors_hash).indent(2)
|
157
|
+
end
|
158
|
+
|
159
|
+
out.puts "\nend"
|
160
|
+
|
161
|
+
out.string
|
162
|
+
end
|
163
|
+
|
164
|
+
def write(dir)
|
165
|
+
file = "#{dir}/#{basename}.tmTheme.rb"
|
166
|
+
File.open(file, 'wb:utf-8') do |f|
|
167
|
+
f.puts '# encoding: utf-8'
|
168
|
+
f.puts "\n" << to_dsl
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def export(dir)
|
173
|
+
file = "#{dir}/#{basename}.tmTheme"
|
174
|
+
PListWriter.new(self).export(file)
|
175
|
+
end
|
176
|
+
|
177
|
+
# A Hash { <color value> => <color name> }.
|
178
|
+
def colors_hash
|
179
|
+
@colors_hash ||= begin
|
180
|
+
colors = base_colors.values + items.flat_map(&:colors)
|
181
|
+
colors.uniq!
|
182
|
+
h = {}
|
183
|
+
colors.each_with_index { |c, i| h[c] = COLOR_NAMES_HASH[c] || "color#{i}" }
|
184
|
+
h
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
__END__
|
194
|
+
|
195
|
+
= Red
|
196
|
+
|
197
|
+
IndianRed CD5C5C
|
198
|
+
LightCoral F08080
|
199
|
+
Salmon FA8072
|
200
|
+
DarkSalmon E9967A
|
201
|
+
LightSalmon FFA07A
|
202
|
+
Crimson DC143C
|
203
|
+
Red FF0000
|
204
|
+
FireBrick B22222
|
205
|
+
DarkRed 8B0000
|
206
|
+
|
207
|
+
= Pink
|
208
|
+
|
209
|
+
Pink FFC0CB
|
210
|
+
LightPink FFB6C1
|
211
|
+
HotPink FF69B4
|
212
|
+
DeepPink FF1493
|
213
|
+
MediumVioletRed C71585
|
214
|
+
PaleVioletRed DB7093
|
215
|
+
|
216
|
+
= Orange
|
217
|
+
|
218
|
+
LightSalmon FFA07A
|
219
|
+
Coral FF7F50
|
220
|
+
Tomato FF6347
|
221
|
+
OrangeRed FF4500
|
222
|
+
DarkOrange FF8C00
|
223
|
+
Orange FFA500
|
224
|
+
|
225
|
+
= Yellow
|
226
|
+
|
227
|
+
Gold FFD700
|
228
|
+
Yellow FFFF00
|
229
|
+
LightYellow FFFFE0
|
230
|
+
LemonChiffon FFFACD
|
231
|
+
LightGoldenrodYellow FAFAD2
|
232
|
+
PapayaWhip FFEFD5
|
233
|
+
Moccasin FFE4B5
|
234
|
+
PeachPuff FFDAB9
|
235
|
+
PaleGoldenrod EEE8AA
|
236
|
+
Khaki F0E68C
|
237
|
+
DarkKhaki BDB76B
|
238
|
+
|
239
|
+
= Purple
|
240
|
+
|
241
|
+
Lavender E6E6FA
|
242
|
+
Thistle D8BFD8
|
243
|
+
Plum DDA0DD
|
244
|
+
Violet EE82EE
|
245
|
+
Orchid DA70D6
|
246
|
+
Fuchsia FF00FF
|
247
|
+
Magenta FF00FF
|
248
|
+
MediumOrchid BA55D3
|
249
|
+
MediumPurple 9370DB
|
250
|
+
BlueViolet 8A2BE2
|
251
|
+
DarkViolet 9400D3
|
252
|
+
DarkOrchid 9932CC
|
253
|
+
DarkMagenta 8B008B
|
254
|
+
Purple 800080
|
255
|
+
Indigo 4B0082
|
256
|
+
SlateBlue 6A5ACD
|
257
|
+
DarkSlateBlue 483D8B
|
258
|
+
|
259
|
+
= Green
|
260
|
+
|
261
|
+
GreenYellow ADFF2F
|
262
|
+
Chartreuse 7FFF00
|
263
|
+
LawnGreen 7CFC00
|
264
|
+
Lime 00FF00
|
265
|
+
LimeGreen 32CD32
|
266
|
+
PaleGreen 98FB98
|
267
|
+
LightGreen 90EE90
|
268
|
+
MediumSpringGreen 00FA9A
|
269
|
+
SpringGreen 00FF7F
|
270
|
+
MediumSeaGreen 3CB371
|
271
|
+
SeaGreen 2E8B57
|
272
|
+
ForestGreen 228B22
|
273
|
+
Green 008000
|
274
|
+
DarkGreen 006400
|
275
|
+
YellowGreen 9ACD32
|
276
|
+
OliveDrab 6B8E23
|
277
|
+
Olive 808000
|
278
|
+
DarkOliveGreen 556B2F
|
279
|
+
MediumAquamarine 66CDAA
|
280
|
+
DarkSeaGreen 8FBC8F
|
281
|
+
LightSeaGreen 20B2AA
|
282
|
+
DarkCyan 008B8B
|
283
|
+
Teal 008080
|
284
|
+
|
285
|
+
= Blue
|
286
|
+
|
287
|
+
Aqua 00FFFF
|
288
|
+
Cyan 00FFFF
|
289
|
+
LightCyan E0FFFF
|
290
|
+
PaleTurquoise AFEEEE
|
291
|
+
Aquamarine 7FFFD4
|
292
|
+
Turquoise 40E0D0
|
293
|
+
MediumTurquoise 48D1CC
|
294
|
+
DarkTurquoise 00CED1
|
295
|
+
CadetBlue 5F9EA0
|
296
|
+
SteelBlue 4682B4
|
297
|
+
LightSteelBlue B0C4DE
|
298
|
+
PowderBlue B0E0E6
|
299
|
+
LightBlue ADD8E6
|
300
|
+
SkyBlue 87CEEB
|
301
|
+
LightSkyBlue 87CEFA
|
302
|
+
DeepSkyBlue 00BFFF
|
303
|
+
DodgerBlue 1E90FF
|
304
|
+
CornflowerBlue 6495ED
|
305
|
+
MediumSlateBlue 7B68EE
|
306
|
+
RoyalBlue 4169E1
|
307
|
+
Blue 0000FF
|
308
|
+
MediumBlue 0000CD
|
309
|
+
DarkBlue 00008B
|
310
|
+
Navy 000080
|
311
|
+
MidnightBlue 191970
|
312
|
+
|
313
|
+
= Brown
|
314
|
+
|
315
|
+
Cornsilk FFF8DC
|
316
|
+
BlanchedAlmond FFEBCD
|
317
|
+
Bisque FFE4C4
|
318
|
+
NavajoWhite FFDEAD
|
319
|
+
Wheat F5DEB3
|
320
|
+
BurlyWood DEB887
|
321
|
+
Tan D2B48C
|
322
|
+
RosyBrown BC8F8F
|
323
|
+
SandyBrown F4A460
|
324
|
+
Goldenrod DAA520
|
325
|
+
DarkGoldenrod B8860B
|
326
|
+
Peru CD853F
|
327
|
+
Chocolate D2691E
|
328
|
+
SaddleBrown 8B4513
|
329
|
+
Sienna A0522D
|
330
|
+
Brown A52A2A
|
331
|
+
Maroon 800000
|
332
|
+
|
333
|
+
= White
|
334
|
+
|
335
|
+
White FFFFFF
|
336
|
+
Snow FFFAFA
|
337
|
+
Honeydew F0FFF0
|
338
|
+
MintCream F5FFFA
|
339
|
+
Azure F0FFFF
|
340
|
+
AliceBlue F0F8FF
|
341
|
+
GhostWhite F8F8FF
|
342
|
+
WhiteSmoke F5F5F5
|
343
|
+
Seashell FFF5EE
|
344
|
+
Beige F5F5DC
|
345
|
+
OldLace FDF5E6
|
346
|
+
FloralWhite FFFAF0
|
347
|
+
Ivory FFFFF0
|
348
|
+
AntiqueWhite FAEBD7
|
349
|
+
Linen FAF0E6
|
350
|
+
LavenderBlush FFF0F5
|
351
|
+
MistyRose FFE4E1
|
352
|
+
|
353
|
+
= Grey
|
354
|
+
|
355
|
+
Gainsboro DCDCDC
|
356
|
+
LightGrey D3D3D3
|
357
|
+
Silver C0C0C0
|
358
|
+
DarkGray A9A9A9
|
359
|
+
Gray 808080
|
360
|
+
DimGray 696969
|
361
|
+
LightSlateGray 778899
|
362
|
+
SlateGray 708090
|
363
|
+
DarkSlateGray 2F4F4F
|
364
|
+
Black 000000
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
module SublimeDSL
|
3
|
+
module Tools
|
4
|
+
|
5
|
+
##
|
6
|
+
# A class with a minimal set of methods, used for DSL readers
|
7
|
+
# that heavily rely on #method_missing.
|
8
|
+
|
9
|
+
class BlankSlate < Object
|
10
|
+
|
11
|
+
# Methods kept:
|
12
|
+
# - public/protected from BasicObject, except operators
|
13
|
+
# - private from BasicObject
|
14
|
+
# - a few Object/Kernel methods
|
15
|
+
|
16
|
+
KEPT_METHODS = %w(
|
17
|
+
__id__
|
18
|
+
__send__
|
19
|
+
equal?
|
20
|
+
instance_eval
|
21
|
+
instance_exec
|
22
|
+
|
23
|
+
initialize
|
24
|
+
method_missing
|
25
|
+
singleton_method_added
|
26
|
+
singleton_method_removed
|
27
|
+
singleton_method_undefined
|
28
|
+
|
29
|
+
__callee__
|
30
|
+
__method__
|
31
|
+
caller
|
32
|
+
define_singleton_method
|
33
|
+
method
|
34
|
+
object_id
|
35
|
+
p
|
36
|
+
puts
|
37
|
+
raise
|
38
|
+
singleton_methods
|
39
|
+
warn
|
40
|
+
).map(&:to_sym)
|
41
|
+
|
42
|
+
(instance_methods + private_instance_methods - KEPT_METHODS).each do |m|
|
43
|
+
undef_method m
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
module SublimeDSL
|
3
|
+
module Tools
|
4
|
+
|
5
|
+
##
|
6
|
+
# Tools for console interaction.
|
7
|
+
|
8
|
+
class Console
|
9
|
+
|
10
|
+
attr_reader :verbosity
|
11
|
+
attr_accessor :set_ruby_verbosity
|
12
|
+
attr_reader :in_progress
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@verbosity = 1
|
16
|
+
@set_ruby_verbosity = true
|
17
|
+
@in_progress = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(text)
|
21
|
+
end_progress
|
22
|
+
$stderr.puts text
|
23
|
+
end
|
24
|
+
|
25
|
+
def verbosity=(value)
|
26
|
+
if set_ruby_verbosity
|
27
|
+
case value
|
28
|
+
when 0 then $VERBOSE = nil
|
29
|
+
when 1 then $VERBOSE = false
|
30
|
+
when 2 then $VERBOSE = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
@verbosity = value
|
34
|
+
end
|
35
|
+
|
36
|
+
def info(text, force = false)
|
37
|
+
return unless $stdout.tty? && (verbosity > 0 || force)
|
38
|
+
end_progress
|
39
|
+
puts text
|
40
|
+
end
|
41
|
+
|
42
|
+
def width
|
43
|
+
(ENV['COLUMNS'] || 80).to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
def progress(index, count, file, force = false)
|
47
|
+
return unless $stdout.tty? && (verbosity == 2 || force)
|
48
|
+
@in_progress = true
|
49
|
+
c = count.to_s
|
50
|
+
prefix = "file %#{c.length}d/#{c}: " % index
|
51
|
+
line = "#{prefix}#{file}"
|
52
|
+
padding = width - line.length - 1
|
53
|
+
if padding < 0
|
54
|
+
file = file[(-padding) .. -1]
|
55
|
+
file[0..2] = "..."
|
56
|
+
line = "#{prefix}#{file}"
|
57
|
+
else
|
58
|
+
line << ' ' * padding
|
59
|
+
end
|
60
|
+
print "#{line}\r"
|
61
|
+
$stdout.flush
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def end_progress
|
67
|
+
puts() if in_progress
|
68
|
+
@in_progress = false
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
|
2
|
+
module SublimeDSL
|
3
|
+
module Tools
|
4
|
+
|
5
|
+
##
|
6
|
+
# Helper methods to include in DSL blocks as needed.
|
7
|
+
|
8
|
+
module Helpers
|
9
|
+
|
10
|
+
# Return an optimized regexp string matching a word in +words+. Example:
|
11
|
+
# include SublimeDSL::Tools::Helpers
|
12
|
+
# list = %w(abs addr addrlong airy allcomb allperm anyalnum anyalpha anycntrl)
|
13
|
+
# optimized_re(list)
|
14
|
+
# => 'a(bs|ddr(long)?|iry|ll(comb|perm)|ny(al(num|pha)|cntrl))'
|
15
|
+
|
16
|
+
def optimized_re(words)
|
17
|
+
root = Node.new('')
|
18
|
+
root.add_children words
|
19
|
+
root.reduce!
|
20
|
+
root.to_re
|
21
|
+
end
|
22
|
+
|
23
|
+
module_function :optimized_re
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# A node in the optimization of a Regexp matching a list of keywords.
|
29
|
+
|
30
|
+
class Node
|
31
|
+
|
32
|
+
# The start string.
|
33
|
+
attr_reader :start
|
34
|
+
|
35
|
+
# The child nodes.
|
36
|
+
attr_reader :children
|
37
|
+
|
38
|
+
# Creates a new node for the +start+ string, with an empty list of children.
|
39
|
+
def initialize(start)
|
40
|
+
@start = start
|
41
|
+
@children = []
|
42
|
+
end
|
43
|
+
|
44
|
+
# The node representing the end of a string: an empty string with no child.
|
45
|
+
NULL = self.new('')
|
46
|
+
|
47
|
+
# Adds +words+ to the children of the current node:
|
48
|
+
# the children of the current node are the unique initial letters of words,
|
49
|
+
# and the following letters are recurisvely added as grandchildren:
|
50
|
+
# x = Node.new('')
|
51
|
+
# x.add_children %w(a abc abd abde)
|
52
|
+
# => tree:
|
53
|
+
# ""
|
54
|
+
# "a"
|
55
|
+
# ""
|
56
|
+
# "b"
|
57
|
+
# "c"
|
58
|
+
# ""
|
59
|
+
# "d"
|
60
|
+
# ""
|
61
|
+
# "e"
|
62
|
+
# ""
|
63
|
+
|
64
|
+
def add_children(words)
|
65
|
+
words.group_by { |w| w[0] }.each_pair do |letter, list|
|
66
|
+
if letter.nil?
|
67
|
+
self.children << Node::NULL
|
68
|
+
else
|
69
|
+
n = Node.new(letter.dup)
|
70
|
+
self.children << n
|
71
|
+
n.add_children list.map { |w| w[1..-1] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Reduce the current node:
|
77
|
+
# - first reduce all children
|
78
|
+
# - then if there is only one +child+ left:
|
79
|
+
# - append the child#start value to self#start
|
80
|
+
# - replace the children of +self+ by the children of +child+
|
81
|
+
# Example:
|
82
|
+
# x = Node.new('')
|
83
|
+
# x.add_children %w(abs addr addrlong airy allcomb allperm anyalnum anyalpha anycntrl)
|
84
|
+
# x.reduce!
|
85
|
+
# => tree:
|
86
|
+
# "a"
|
87
|
+
# "bs"
|
88
|
+
# "ddr"
|
89
|
+
# ""
|
90
|
+
# "long"
|
91
|
+
# "iry"
|
92
|
+
# "ll"
|
93
|
+
# "comb"
|
94
|
+
# "perm"
|
95
|
+
# "ny"
|
96
|
+
# "al"
|
97
|
+
# "num"
|
98
|
+
# "pha"
|
99
|
+
# "cntrl"
|
100
|
+
|
101
|
+
def reduce!
|
102
|
+
children.each(&:reduce!)
|
103
|
+
if children.length == 1
|
104
|
+
child = children.first
|
105
|
+
@start << child.start
|
106
|
+
@children = child.children
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Converts the current node to a regular expression string:
|
111
|
+
# x = Node.new('')
|
112
|
+
# x.add_children %w(abs addr addrlong airy allcomb allperm anyalnum anyalpha anycntrl)
|
113
|
+
# x.reduce!
|
114
|
+
# x.to_re
|
115
|
+
# => 'a(bs|ddr(long)?|iry|ll(comb|perm)|ny(al(num|pha)|cntrl))'
|
116
|
+
|
117
|
+
def to_re
|
118
|
+
t, list = children.partition { |c| c == NULL }
|
119
|
+
t = t.first
|
120
|
+
start + (
|
121
|
+
case list.length
|
122
|
+
when 0
|
123
|
+
''
|
124
|
+
when 1
|
125
|
+
r = list.first.to_re
|
126
|
+
r.length == 1 ? r : '(' + r + ')'
|
127
|
+
else
|
128
|
+
r = list.map(&:to_re)
|
129
|
+
if r.all? { |s| s.length == 1 }
|
130
|
+
'[' + r.join + ']'
|
131
|
+
else
|
132
|
+
'(' + r.join('|') + ')'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
) + (
|
136
|
+
( t ? '?' : '' )
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
# The display tree for the current node.
|
141
|
+
|
142
|
+
def display_tree(indent = '')
|
143
|
+
return indent + start.inspect if children.empty?
|
144
|
+
[ indent + start.inspect,
|
145
|
+
children.map { |c| c.display_tree(indent+' ') },
|
146
|
+
].join("\n")
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|