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.
- 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
|