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