Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
marvel
GitHub Repository: marvel/qnf
Path: blob/master/elisp/emacs-for-python/yasnippet/extras/textmate_import.rb
990 views
1
#!/usr/bin/ruby
2
# -*- coding: utf-8 -*-
3
#!/usr/bin/env ruby
4
# -*- coding: utf-8 -*-
5
# textmate_import.rb --- import textmate snippets
6
#
7
# Copyright (C) 2009 Rob Christie, 2010 João Távora
8
#
9
# This is a quick script to generate YASnippets from TextMate Snippets.
10
#
11
# I based the script off of a python script of a similar nature by
12
# Jeff Wheeler: http://nokrev.com
13
# http://code.nokrev.com/?p=snippet-copier.git;a=blob_plain;f=snippet_copier.py
14
#
15
# Use textmate_import.rb --help to get usage information.
16
17
require 'rubygems'
18
require 'plist'
19
require 'choice'
20
require 'fileutils'
21
require 'shellwords' # String#shellescape
22
require 'ruby-debug' if $DEBUG
23
24
Choice.options do
25
header ''
26
header 'Standard Options:'
27
28
option :bundle_dir do
29
short '-d'
30
long '--bundle-dir=PATH'
31
desc 'Tells the program the directory to find the TextMate bundle directory'
32
default '.'
33
end
34
35
option :output_dir do
36
short '-o'
37
long '--output-dir=PATH'
38
desc 'What directory to write the new YASnippets to'
39
end
40
41
option :snippet do
42
short '-f'
43
long '--file=SNIPPET FILE NAME'
44
desc 'A specific snippet that you want to copy or a glob for various files'
45
default '*.{tmSnippet,tmCommand,plist,tmMacro}'
46
end
47
48
option :print_pretty do
49
short '-p'
50
long '--pretty-print'
51
desc 'Pretty prints multiple snippets when printing to standard out'
52
end
53
54
option :quiet do
55
short '-q'
56
long '--quiet'
57
desc 'Be quiet.'
58
end
59
60
option :convert_bindings do
61
short '-b'
62
long '--convert-bindings'
63
desc "TextMate \"keyEquivalent\" keys are translated to YASnippet \"# binding :\" directives"
64
end
65
66
option :info_plist do
67
short '-g'
68
long '--info-plist=PLIST'
69
desc "Specify a plist file derive menu information from defaults to \"bundle-dir\"/info.plist"
70
end
71
72
separator ''
73
separator 'Common options: '
74
75
option :help do
76
long '--help'
77
desc 'Show this message'
78
end
79
end
80
81
# Represents and is capable of outputting the representation of a
82
# TextMate menu in terms of `yas/define-menu'
83
#
84
class TmSubmenu
85
86
@@excluded_items = [];
87
def self.excluded_items; @@excluded_items; end
88
89
attr_reader :items, :name
90
def initialize(name, hash)
91
@items = hash["items"]
92
@name = name
93
end
94
95
def to_lisp(allsubmenus,
96
deleteditems,
97
indent = 0,
98
thingy = ["(", ")"])
99
100
first = true;
101
102
string = ""
103
separator_useless = true;
104
items.each do |uuid|
105
if deleteditems.index(uuid)
106
$stderr.puts "#{uuid} has been deleted!"
107
next
108
end
109
string += "\n"
110
string += " " * indent
111
string += (first ? thingy[0] : (" " * thingy[0].length))
112
113
submenu = allsubmenus[uuid]
114
snippet = TmSnippet::snippets_by_uid[uuid]
115
unimplemented = TmSnippet::unknown_substitutions["content"][uuid]
116
if submenu
117
str = "(yas/submenu "
118
string += str + "\"" + submenu.name + "\""
119
string += submenu.to_lisp(allsubmenus, deleteditems,
120
indent + str.length + thingy[0].length)
121
elsif snippet and not unimplemented
122
string += ";; " + snippet.name + "\n"
123
string += " " * (indent + thingy[0].length)
124
string += "(yas/item \"" + uuid + "\")"
125
separator_useless = false;
126
elsif snippet and unimplemented
127
string += ";; Ignoring " + snippet.name + "\n"
128
string += " " * (indent + thingy[0].length)
129
string += "(yas/ignore-item \"" + uuid + "\")"
130
separator_useless = true;
131
elsif (uuid =~ /---------------------/)
132
string += "(yas/separator)" unless separator_useless
133
end
134
first = false;
135
end
136
string += ")"
137
string += thingy[1]
138
139
return string
140
end
141
142
def self.main_menu_to_lisp (parsed_plist, modename)
143
mainmenu = parsed_plist["mainMenu"]
144
deleted = parsed_plist["deleted"]
145
146
root = TmSubmenu.new("__main_menu__", mainmenu)
147
all = {}
148
149
mainmenu["submenus"].each_pair do |k,v|
150
all[k] = TmSubmenu.new(v["name"], v)
151
end
152
153
excluded = mainmenu["excludedItems"] + TmSubmenu::excluded_items
154
closing = "\n '("
155
closing+= excluded.collect do |uuid|
156
"\"" + uuid + "\""
157
end.join( "\n ") + "))"
158
159
str = "(yas/define-menu "
160
return str + "'#{modename}" + root.to_lisp(all,
161
deleted,
162
str.length,
163
["'(" , closing])
164
end
165
end
166
167
168
# Represents a textmate snippet
169
#
170
# - @file is the .tmsnippet/.plist file path relative to cwd
171
#
172
# - optional @info is a Plist.parsed info.plist found in the bundle dir
173
#
174
# - @@snippets_by_uid is where one can find all the snippets parsed so
175
# far.
176
#
177
#
178
class SkipSnippet < RuntimeError; end
179
class TmSnippet
180
@@known_substitutions = {
181
"content" => {
182
"${TM_RAILS_TEMPLATE_START_RUBY_EXPR}" => "<%= ",
183
"${TM_RAILS_TEMPLATE_END_RUBY_EXPR}" => " %>",
184
"${TM_RAILS_TEMPLATE_START_RUBY_INLINE}" => "<% ",
185
"${TM_RAILS_TEMPLATE_END_RUBY_INLINE}" => " -%>",
186
"${TM_RAILS_TEMPLATE_END_RUBY_BLOCK}" => "end" ,
187
"${0:$TM_SELECTED_TEXT}" => "${0:`yas/selected-text`}",
188
/\$\{(\d+)\}/ => "$\\1",
189
"${1:$TM_SELECTED_TEXT}" => "${1:`yas/selected-text`}",
190
"${2:$TM_SELECTED_TEXT}" => "${2:`yas/selected-text`}",
191
'$TM_SELECTED_TEXT' => "`yas/selected-text`",
192
%r'\$\{TM_SELECTED_TEXT:([^\}]*)\}' => "`(or (yas/selected-text) \"\\1\")`",
193
%r'`[^`]+\n[^`]`' => Proc.new {|uuid, match| "(yas/multi-line-unknown " + uuid + ")"}},
194
"condition" => {
195
/^source\..*$/ => "" },
196
"binding" => {},
197
"type" => {}
198
}
199
200
def self.extra_substitutions; @@extra_substitutions; end
201
@@extra_substitutions = {
202
"content" => {},
203
"condition" => {},
204
"binding" => {},
205
"type" => {}
206
}
207
208
def self.unknown_substitutions; @@unknown_substitutions; end
209
@@unknown_substitutions = {
210
"content" => {},
211
"condition" => {},
212
"binding" => {},
213
"type" => {}
214
}
215
216
@@snippets_by_uid={}
217
def self.snippets_by_uid; @@snippets_by_uid; end
218
219
def initialize(file,info=nil)
220
@file = file
221
@info = info
222
@snippet = TmSnippet::read_plist(file)
223
@@snippets_by_uid[self.uuid] = self;
224
raise SkipSnippet.new "not a snippet/command/macro." unless (@snippet["scope"] || @snippet["command"])
225
raise SkipSnippet.new "looks like preferences."if @file =~ /Preferences\//
226
raise RuntimeError.new("Cannot convert this snippet #{file}!") unless @snippet;
227
end
228
229
def name
230
@snippet["name"]
231
end
232
233
def uuid
234
@snippet["uuid"]
235
end
236
237
def key
238
@snippet["tabTrigger"]
239
end
240
241
def condition
242
yas_directive "condition"
243
end
244
245
def type
246
override = yas_directive "type"
247
if override
248
return override
249
else
250
return "# type: command\n" if @file =~ /(Commands\/|Macros\/)/
251
end
252
end
253
254
def binding
255
yas_directive "binding"
256
end
257
258
def content
259
known = @@known_substitutions["content"]
260
extra = @@extra_substitutions["content"]
261
if direct = extra[uuid]
262
return direct
263
else
264
ct = @snippet["content"]
265
if ct
266
known.each_pair do |k,v|
267
if v.respond_to? :call
268
ct.gsub!(k) {|match| v.call(uuid, match)}
269
else
270
ct.gsub!(k,v)
271
end
272
end
273
extra.each_pair do |k,v|
274
ct.gsub!(k,v)
275
end
276
# the remaining stuff is an unknown substitution
277
#
278
[ %r'\$\{ [^/\}\{:]* / [^/]* / [^/]* / [^\}]*\}'x ,
279
%r'\$\{[^\d][^}]+\}',
280
%r'`[^`]+`',
281
%r'\$TM_[\w_]+',
282
%r'\(yas/multi-line-unknown [^\)]*\)'
283
].each do |reg|
284
ct.scan(reg) do |match|
285
@@unknown_substitutions["content"][match] = self
286
end
287
end
288
return ct
289
else
290
@@unknown_substitutions["content"][uuid] = self
291
TmSubmenu::excluded_items.push(uuid)
292
return "(yas/unimplemented)"
293
end
294
end
295
end
296
297
def to_yas
298
doc = "# -*- mode: snippet -*-\n"
299
doc << (self.type || "")
300
doc << "# uuid: #{self.uuid}\n"
301
doc << "# key: #{self.key}\n" if self.key
302
doc << "# contributor: Translated from textmate snippet by PROGRAM_NAME\n"
303
doc << "# name: #{self.name}\n"
304
doc << (self.binding || "")
305
doc << (self.condition || "")
306
doc << "# --\n"
307
doc << (self.content || "(yas/unimplemented)")
308
doc
309
end
310
311
def self.canonicalize(filename)
312
invalid_char = /[^ a-z_0-9.+=~(){}\/'`&#,-]/i
313
314
filename.
315
gsub(invalid_char, ''). # remove invalid characters
316
gsub(/ {2,}/,' '). # squeeze repeated spaces into a single one
317
rstrip # remove trailing whitespaces
318
end
319
320
def yas_file()
321
File.join(TmSnippet::canonicalize(@file[0, @file.length-File.extname(@file).length]) + ".yasnippet")
322
end
323
324
def self.read_plist(xml_or_binary)
325
begin
326
parsed = Plist::parse_xml(xml_or_binary)
327
return parsed if parsed
328
raise ArgumentError.new "Probably in binary format and parse_xml is very quiet..."
329
rescue StandardError => e
330
if (system "plutil -convert xml1 #{xml_or_binary.shellescape} -o /tmp/textmate_import.tmpxml")
331
return Plist::parse_xml("/tmp/textmate_import.tmpxml")
332
else
333
raise RuntimeError.new "plutil failed miserably, check if you have it..."
334
end
335
end
336
end
337
338
private
339
340
@@yas_to_tm_directives = {"condition" => "scope", "binding" => "keyEquivalent", "key" => "tabTrigger"}
341
def yas_directive(yas_directive)
342
#
343
# Merge "known" hardcoded substitution with "extra" substitutions
344
# provided in the .yas-setup.el file.
345
#
346
merged = @@known_substitutions[yas_directive].
347
merge(@@extra_substitutions[yas_directive])
348
#
349
# First look for an uuid-based direct substitution for this
350
# directive.
351
#
352
if direct = merged[uuid]
353
return "# #{yas_directive}: "+ direct + "\n" unless direct.empty?
354
else
355
tm_directive = @@yas_to_tm_directives[yas_directive]
356
val = tm_directive && @snippet[tm_directive]
357
if val and !val.delete(" ").empty? then
358
#
359
# Sort merged substitutions by length (bigger ones first,
360
# regexps last), and apply them to the value gotten for plist.
361
#
362
merged.sort_by do |what, with|
363
if what.respond_to? :length then -what.length else 0 end
364
end.each do |sub|
365
if val.gsub!(sub[0],sub[1])
366
return "# #{yas_directive}: "+ val + "\n" unless val.empty?
367
end
368
end
369
#
370
# If we get here, no substitution matched, so mark this an
371
# unknown substitution.
372
#
373
@@unknown_substitutions[yas_directive][val] = self
374
return "## #{yas_directive}: \""+ val + "\n"
375
end
376
end
377
end
378
379
end
380
381
382
if $0 == __FILE__
383
# Read the the bundle's info.plist if can find it/guess it
384
#
385
info_plist_file = Choice.choices.info_plist || File.join(Choice.choices.bundle_dir,"info.plist")
386
info_plist = TmSnippet::read_plist(info_plist_file) if info_plist_file and File.readable? info_plist_file;
387
388
# Calculate the mode name
389
#
390
modename = File.basename Choice.choices.output_dir || "major-mode-name"
391
392
# Read in .yas-setup.el looking for the separator between auto-generated
393
#
394
original_dir = Dir.pwd
395
yas_setup_el_file = File.join(original_dir, Choice.choices.output_dir, ".yas-setup.el")
396
separator = ";; --**--"
397
whole, head , tail = "", "", ""
398
if File::exists? yas_setup_el_file
399
File.open yas_setup_el_file, 'r' do |file|
400
whole = file.read
401
head , tail = whole.split(separator)
402
end
403
else
404
head = ";; .yas-setup.el for #{modename}\n" + ";; \n"
405
end
406
407
# Now iterate the tail part to find extra substitutions
408
#
409
tail ||= ""
410
head ||= ""
411
directive = nil
412
# puts "get this head #{head}"
413
head.each_line do |line|
414
case line
415
when /^;; Substitutions for:(.*)$/
416
directive = $~[1].strip
417
# puts "found the directove #{directive}"
418
when /^;;(.*)[ ]+=yyas>(.*)$/
419
replacewith = $~[2].strip
420
lookfor = $~[1]
421
lookfor.gsub!(/^[ ]*/, "")
422
lookfor.gsub!(/[ ]*$/, "")
423
# puts "found this wonderful substitution for #{directive} which is #{lookfor} => #{replacewith}"
424
unless !directive or replacewith =~ /yas\/unknown/ then
425
TmSnippet.extra_substitutions[directive][lookfor] = replacewith
426
end
427
end
428
end
429
430
# Glob snippets into snippet_files, going into subdirs
431
#
432
Dir.chdir Choice.choices.bundle_dir
433
snippet_files_glob = File.join("**", Choice.choices.snippet)
434
snippet_files = Dir.glob(snippet_files_glob)
435
436
# Attempt to convert each snippet files in snippet_files
437
#
438
puts "Will try to convert #{snippet_files.length} snippets...\n" unless Choice.choices.quiet
439
440
441
# Iterate the globbed files
442
#
443
snippet_files.each do |file|
444
begin
445
puts "Processing \"#{File.join(Choice.choices.bundle_dir,file)}\"\n" unless Choice.choices.quiet
446
snippet = TmSnippet.new(file,info_plist)
447
448
if
449
file_to_create = File.join(original_dir, Choice.choices.output_dir, snippet.yas_file)
450
FileUtils.mkdir_p(File.dirname(file_to_create))
451
File.open(file_to_create, 'w') do |f|
452
f.write(snippet.to_yas)
453
end
454
else
455
if Choice.choices.print_pretty
456
puts "--------------------------------------------"
457
end
458
puts snippet.to_yas if Choice.choices.print_pretty or not Choice.choices.info_plist
459
if Choice.choices.print_pretty
460
puts "--------------------------------------------\n\n"
461
end
462
end
463
rescue SkipSnippet => e
464
$stdout.puts "Skipping \"#{file}\": #{e.message}"
465
rescue RuntimeError => e
466
$stderr.puts "Oops.... \"#{file}\": #{e.message}"
467
$strerr.puts "#{e.backtrace.join("\n")}" unless Choice.choices.quiet
468
end
469
end
470
471
# Attempt to decypher the menu
472
#
473
menustr = TmSubmenu::main_menu_to_lisp(info_plist, modename) if info_plist
474
puts menustr if $DEBUG
475
476
# Write some basic .yas-* files
477
#
478
if Choice.choices.output_dir
479
FileUtils.mkdir_p Choice.choices.output_dir
480
FileUtils.touch File.join(original_dir, Choice.choices.output_dir, ".yas-make-groups") unless menustr
481
FileUtils.touch File.join(original_dir, Choice.choices.output_dir, ".yas-ignore-filenames-as-triggers")
482
483
# Now, output head + a new tail in (possibly new) .yas-setup.el
484
# file
485
#
486
File.open yas_setup_el_file, 'w' do |file|
487
file.puts head
488
file.puts separator
489
file.puts ";; Automatically generated code, do not edit this part"
490
file.puts ";; "
491
file.puts ";; Translated menu"
492
file.puts ";; "
493
file.puts menustr
494
file.puts
495
file.puts ";; Unknown substitutions"
496
file.puts ";; "
497
["content", "condition", "binding"].each do |type|
498
file.puts ";; Substitutions for: #{type}"
499
file.puts ";; "
500
# TmSnippet::extra_substitutions[type].
501
# each_pair do |k,v|
502
# file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> " + v
503
# end
504
unknown = TmSnippet::unknown_substitutions[type];
505
unknown.keys.uniq.each do |k|
506
file.puts ";; # as in " + unknown[k].yas_file
507
file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> (yas/unknown)"
508
file.puts ";; "
509
end
510
file.puts ";; "
511
file.puts
512
end
513
file.puts ";; .yas-setup.el for #{modename} ends here"
514
end
515
end
516
end
517
518