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