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