]>
code.delx.au - gnu-emacs-elpa/blob - extras/textmate_import.rb
639e508ecb10f76e6034a6baf3c40b9657a86ef3
2 # -*- coding: utf-8 -*-
4 # -*- coding: utf-8 -*-
5 # textmate_import.rb --- import textmate snippets
7 # Copyright (C) 2009 Rob Christie, 2010 João Távora
9 # This is a quick script to generate YASnippets from TextMate Snippets.
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
15 # Use textmate_import.rb --help to get usage information.
21 require 'shellwords' # String#shellescape
22 require 'ruby-debug' if $DEBUG
26 header
'Standard Options:'
30 long
'--bundle-dir=PATH'
31 desc
'Tells the program the directory to find the TextMate bundle directory'
37 long
'--output-dir=PATH'
38 desc
'What directory to write the new YASnippets to'
39 default
'./textmate_import'
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}'
49 option
:print_pretty do
52 desc
'Pretty prints multiple snippets when printing to standard out'
61 option
:convert_bindings do
63 long
'--convert-bindings'
64 desc
"TextMate \"keyEquivalent\" keys are translated to YASnippet \"# binding :\" directives"
69 long
'--info-plist=PLIST'
70 desc
"Specify a plist file derive menu information from defaults to \"bundle-dir\"/info.plist"
74 separator
'Common options: '
78 desc
'Show this message'
82 # Represents and is capable of outputting the representation of a
83 # TextMate menu in terms of `yas/define-menu'
87 @
@excluded_items = [];
88 def self.excluded_items
; @
@excluded_items; end
90 attr_reader
:items, :name
91 def initialize(name
, hash
)
92 @items = hash
["items"]
96 def to_lisp(allsubmenus
,
104 separator_useless
= true;
106 if deleteditems
.index(uuid
)
107 $stderr.puts
"#{uuid} has been deleted!"
111 string
+= " " * indent
112 string
+= (first
? thingy
[0] : (" " * thingy
[0].length
))
114 submenu
= allsubmenus
[uuid
]
115 snippet
= TmSnippet
::snippets_by_uid[uuid
]
116 unimplemented
= TmSnippet
::unknown_substitutions["content"][uuid
]
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
143 def self.main_menu_to_lisp (parsed_plist
, modename
)
144 mainmenu
= parsed_plist
["mainMenu"]
145 deleted
= parsed_plist
["deleted"]
147 root
= TmSubmenu
.new("__main_menu__", mainmenu
)
150 mainmenu
["submenus"].each_pair
do |k
,v
|
151 all
[k
] = TmSubmenu
.new(v
["name"], v
)
154 excluded
= mainmenu
["excludedItems"] + TmSubmenu
::excluded_items
156 closing
+= excluded
.collect
do |uuid
|
158 end.join( "\n ") + "))"
160 str
= "(yas/define-menu "
161 return str
+ "'#{modename}" + root
.to_lisp(all
,
169 # Represents a textmate snippet
171 # - @file is the .tmsnippet/.plist file path relative to cwd
173 # - optional @info is a Plist.parsed info.plist found in the bundle dir
175 # - @@snippets_by_uid is where one can find all the snippets parsed so
179 class SkipSnippet
< RuntimeError
; end
181 @
@known_substitutions = {
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
+ ")"}},
196 /^source\..*$/ => "" },
201 def self.extra_substitutions
; @
@extra_substitutions; end
202 @
@extra_substitutions = {
209 def self.unknown_substitutions
; @
@unknown_substitutions; end
210 @
@unknown_substitutions = {
218 def self.snippets_by_uid
; @
@snippets_by_uid; end
220 def initialize(file
,info
=nil)
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;
239 @snippet["tabTrigger"]
243 yas_directive
"condition"
247 override
= yas_directive
"type"
251 return "# type: command\n" if @file =~
/(Commands\/|Macros\
/)/
256 yas_directive
"binding"
260 known
= @
@known_substitutions["content"]
261 extra
= @
@extra_substitutions["content"]
262 if direct
= extra
[uuid
]
265 ct
= @snippet["content"]
267 known
.each_pair
do |k
,v
|
268 if v
.respond_to
? :call
269 ct
.gsub
!(k
) {|match
| v
.call(uuid
, match
)}
274 extra
.each_pair
do |k
,v
|
277 # the remaining stuff is an unknown substitution
279 [ %r
'\$\{ [^/\}\{:]* / [^/]* / [^/]* / [^\}]*\}'x
,
280 %r
'\$\{[^\d][^}]+\}',
283 %r
'\(yas/multi-line-unknown [^\)]*\)'
285 ct
.scan(reg
) do |match
|
286 @
@unknown_substitutions["content"][match
] = self
291 @
@unknown_substitutions["content"][uuid
] = self
292 TmSubmenu
::excluded_items.push(uuid
)
293 return "(yas/unimplemented)"
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
|| "")
308 doc
<< (self.content
|| "(yas/unimplemented)")
312 def self.canonicalize(filename
)
313 invalid_char
= /[^ a-z_0-9.+=~(){}\/'`&#,-]/i
316 gsub(invalid_char, ''). # remove invalid characters
317 gsub(/ {2,}/,' '). # squeeze repeated spaces into a single one
318 rstrip # remove trailing whitespaces
322 File.join(TmSnippet::canonicalize(@file[0, @file.length-File.extname(@file).length]) + ".yasnippet")
325 def self.read_plist(xml_or_binary)
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")
334 raise RuntimeError.new "plutil failed miserably, check if you have it..."
341 @@yas_to_tm_directives = {"condition" => "scope", "binding" => "keyEquivalent", "key" => "tabTrigger"}
342 def yas_directive(yas_directive)
344 # Merge "known" hardcoded substitution with "extra" substitutions
345 # provided in the .yas-setup.el file.
347 merged = @@known_substitutions[yas_directive].
348 merge(@@extra_substitutions[yas_directive])
350 # First look for an uuid-based direct substitution for this
353 if direct = merged[uuid]
354 return "# #{yas_directive}: "+ direct + "\n" unless direct.empty?
356 tm_directive = @@yas_to_tm_directives[yas_directive]
357 val = tm_directive && @snippet[tm_directive]
358 if val and !val.delete(" ").empty? then
360 # Sort merged substitutions by length (bigger ones first,
361 # regexps last), and apply them to the value gotten for plist.
363 merged.sort_by do |what, with|
364 if what.respond_to? :length then -what.length else 0 end
366 if val.gsub!(sub[0],sub[1])
367 return "# #{yas_directive}: "+ val + "\n" unless val.empty?
371 # If we get here, no substitution matched, so mark this an
372 # unknown substitution.
374 @@unknown_substitutions[yas_directive][val] = self
375 return "## #{yas_directive}: \""+ val + "\n"
383 if __FILE__ == $PROGRAM_NAME
384 # Read the the bundle's info
.plist
if can find it
/guess it
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
;
389 # Calculate the mode name
391 modename
= File
.basename Choice
.choices
.output_dir
|| "major-mode-name"
393 # Read in .yas-setup.el looking for the separator between auto-generated
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
|
402 head
, tail
= whole
.split(separator
)
405 head
= ";; .yas-setup.el for #{modename}\n" + ";; \n"
408 # Now iterate the tail part to find extra substitutions
413 # puts "get this head #{head}"
414 head
.each_line
do |line
|
416 when /^;; Substitutions for:(.*)$/
417 directive
= $~
[1].strip
418 # puts "found the directove #{directive}"
419 when /^;;(.*)[ ]+=yyas>(.*)$/
420 replacewith
= $~
[2].strip
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
431 # Glob snippets into snippet_files, going into subdirs
433 Dir
.chdir Choice
.choices
.bundle_dir
434 snippet_files_glob
= File
.join("**", Choice
.choices
.snippet
)
435 snippet_files
= Dir
.glob(snippet_files_glob
)
437 # Attempt to convert each snippet files in snippet_files
439 puts
"Will try to convert #{snippet_files.length} snippets...\n" unless Choice
.choices
.quiet
442 # Iterate the globbed files
444 snippet_files
.each
do |file
|
446 puts
"Processing \"#{File.join(Choice.choices.bundle_dir,file)}\"\n" unless Choice
.choices
.quiet
447 snippet
= TmSnippet
.new(file
,info_plist
)
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
)
456 if Choice
.choices
.print_pretty
457 puts
"--------------------------------------------"
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"
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
472 # Attempt to decypher the menu
474 menustr
= TmSubmenu
::main_menu_to_lisp(info_plist
, modename
) if info_plist
475 puts menustr
if $DEBUG
477 # Write some basic .yas-* files
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")
484 # Now, output head + a new tail in (possibly new) .yas-setup.el
487 File
.open yas_setup_el_file
, 'w' do |file
|
490 file
.puts
";; Automatically generated code, do not edit this part"
492 file
.puts
";; Translated menu"
496 file
.puts
";; Unknown substitutions"
498 ["content", "condition", "binding"].each
do |type
|
499 file
.puts
";; Substitutions for: #{type}"
501 # TmSnippet::extra_substitutions[type].
503 # file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> " + v
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)"
514 file
.puts
";; .yas-setup.el for #{modename} ends here"