]>
code.delx.au - gnu-emacs-elpa/blob - packages/yasnippet/extras/textmate_import.rb
2 # -*- coding: utf-8 -*-
3 # textmate_import.rb --- import textmate snippets
5 # Copyright (C) 2009 Rob Christie, 2010 João Távora
7 # This is a quick script to generate YASnippets from TextMate Snippets.
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
13 # Use textmate_import.rb --help to get usage information.
19 require 'shellwords' # String#shellescape
20 require 'ruby-debug' if $DEBUG
22 Encoding
.default_external
= Encoding
::UTF_8 if RUBY_VERSION > '1.8.7'
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
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
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
38 Trollop
::die :plist_file, "must exist" if opts
.plist_file
&& File
.directory
?(opts
.plist_file
)
41 # Represents and is capable of outputting the representation of a
42 # TextMate menu in terms of `yas-define-menu'
46 @
@excluded_items = [];
47 def self.excluded_items
; @
@excluded_items; end
49 attr_reader
:items, :name
50 def initialize(name
, hash
)
51 @items = hash
["items"]
55 def to_lisp(allsubmenus
,
63 separator_useless
= true;
65 if deleteditems
&& deleteditems
.index(uuid
)
66 $stderr.puts
"#{uuid} has been deleted!"
70 string
+= " " * indent
71 string
+= (first
? thingy
[0] : (" " * thingy
[0].length
))
73 submenu
= allsubmenus
[uuid
]
74 snippet
= TmSnippet
::snippets_by_uid[uuid
]
75 unimplemented
= TmSnippet
::unknown_substitutions["content"][uuid
]
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
102 def self.main_menu_to_lisp (parsed_plist
, modename
)
103 mainmenu
= parsed_plist
["mainMenu"]
104 deleted
= parsed_plist
["deleted"]
106 root
= TmSubmenu
.new("__main_menu__", mainmenu
)
109 mainmenu
["submenus"].each_pair
do |k
,v
|
110 all
[k
] = TmSubmenu
.new(v
["name"], v
)
113 excluded
= (mainmenu
["excludedItems"] || []) + TmSubmenu
::excluded_items
115 closing
+= excluded
.collect
do |uuid
|
117 end.join( "\n ") + "))"
119 str
= "(yas-define-menu "
120 return str
+ "'#{modename}" + root
.to_lisp(all
,
128 # Represents a textmate snippet
130 # - @file is the .tmsnippet/.plist file path relative to cwd
132 # - optional @info is a Plist.parsed info.plist found in the bundle dir
134 # - @@snippets_by_uid is where one can find all the snippets parsed so
138 class SkipSnippet
< RuntimeError
; end
140 @
@known_substitutions = {
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
+ ")"}},
155 /^source\..*$/ => "" },
160 def self.extra_substitutions
; @
@extra_substitutions; end
161 @
@extra_substitutions = {
168 def self.unknown_substitutions
; @
@unknown_substitutions; end
169 @
@unknown_substitutions = {
177 def self.snippets_by_uid
; @
@snippets_by_uid; end
179 def initialize(file
,info
=nil)
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;
198 @snippet["tabTrigger"]
202 yas_directive
"condition"
206 override
= yas_directive
"type"
210 return "# type: command\n" if @file =~
/(Commands\/|Macros\
/)/
215 yas_directive
"binding"
219 known
= @
@known_substitutions["content"]
220 extra
= @
@extra_substitutions["content"]
221 if direct
= extra
[uuid
]
224 ct
= @snippet["content"]
226 known
.each_pair
do |k
,v
|
227 if v
.respond_to
? :call
228 ct
.gsub
!(k
) {|match
| v
.call(uuid
, match
)}
233 extra
.each_pair
do |k
,v
|
236 # the remaining stuff is an unknown substitution
238 [ %r
'\$\{ [^/\}\{:]* / [^/]* / [^/]* / [^\}]*\}'x
,
239 %r
'\$\{[^\d][^}]+\}',
242 %r
'\(yas-multi-line-unknown [^\)]*\)'
244 ct
.scan(reg
) do |match
|
245 @
@unknown_substitutions["content"][match
] = self
250 @
@unknown_substitutions["content"][uuid
] = self
251 TmSubmenu
::excluded_items.push(uuid
)
252 return "(yas-unimplemented)"
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
|| "")
267 doc
<< (self.content
|| "(yas-unimplemented)")
271 def self.canonicalize(filename
)
272 invalid_char
= /[^ a-z_0-9.+=~(){}\/'`&#,-]/i
275 gsub(invalid_char, ''). # remove invalid characters
276 gsub(/ {2,}/,' '). # squeeze repeated spaces into a single one
277 rstrip # remove trailing whitespaces
281 File.join(TmSnippet::canonicalize(@file[0, @file.length-File.extname(@file).length]) + ".yasnippet")
284 def self.read_plist(xml_or_binary)
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")
293 raise RuntimeError.new "plutil failed miserably, check if you have it..."
300 @@yas_to_tm_directives = {"condition" => "scope", "binding" => "keyEquivalent", "key" => "tabTrigger"}
301 def yas_directive(yas_directive)
303 # Merge "known" hardcoded substitution with "extra" substitutions
304 # provided in the .yas-setup.el file.
306 merged = @@known_substitutions[yas_directive].
307 merge(@@extra_substitutions[yas_directive])
309 # First look for an uuid-based direct substitution for this
312 if direct = merged[uuid]
313 return "# #{yas_directive}: "+ direct + "\n" unless direct.empty?
315 tm_directive = @@yas_to_tm_directives[yas_directive]
316 val = tm_directive && @snippet[tm_directive]
317 if val and !val.delete(" ").empty? then
319 # Sort merged substitutions by length (bigger ones first,
320 # regexps last), and apply them to the value gotten for plist.
322 allsubs = merged.sort_by do |what, with|
323 if what.respond_to? :length then -what.length else 0 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?
332 # If we get here, no substitution matched, so mark this an
333 # unknown substitution.
335 @@unknown_substitutions[yas_directive][val] = self
336 return "## #{yas_directive}: \""+ val + "\n"
344 if __FILE__ == $PROGRAM_NAME
345 # Read the the bundle's info
.plist
if can find it
/guess it
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
;
350 # Calculate the mode name
352 modename
= File
.basename opts
.output_dir
|| "major-mode-name"
354 # Read in .yas-setup.el looking for the separator between auto-generated
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
|
363 head
, tail
= whole
.split(separator
)
366 head
= ";; .yas-setup.el for #{modename}\n" + ";; \n"
369 # Now iterate the tail part to find extra substitutions
374 # puts "get this head #{head}"
375 head
.each_line
do |line
|
377 when /^;; Substitutions for:(.*)$/
378 directive
= $~
[1].strip
379 # puts "found the directove #{directive}"
380 when /^;;(.*)[ ]+=yyas>(.*)$/
381 replacewith
= $~
[2].strip
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
392 # Glob snippets into snippet_files, going into subdirs
394 Dir
.chdir opts
.bundle_dir
395 snippet_files_glob
= File
.join("**", opts
.glob
)
396 snippet_files
= Dir
.glob(snippet_files_glob
)
398 # Attempt to convert each snippet files in snippet_files
400 puts
"Will try to convert #{snippet_files.length} snippets...\n" unless opts
.quiet
403 # Iterate the globbed files
405 snippet_files
.each
do |file
|
407 $stdout.print
"Processing \"#{File.join(opts.bundle_dir,file)}\"..." unless opts
.quiet
408 snippet
= TmSnippet
.new(file
,info_plist
)
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
)
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
424 # Attempt to decypher the menu
426 menustr
= TmSubmenu
::main_menu_to_lisp(info_plist
, modename
) if info_plist
427 puts menustr
if $DEBUG
429 # Write some basic .yas-* files
432 FileUtils
.mkdir_p opts
.output_dir
433 FileUtils
.touch File
.join(original_dir
, opts
.output_dir
, ".yas-make-groups") unless menustr
435 # Now, output head + a new tail in (possibly new) .yas-setup.el
438 File
.open yas_setup_el_file
, 'w' do |file
|
441 file
.puts
";; Automatically generated code, do not edit this part"
443 file
.puts
";; Translated menu"
447 file
.puts
";; Unknown substitutions"
449 ["content", "condition", "binding"].each
do |type
|
450 file
.puts
";; Substitutions for: #{type}"
452 # TmSnippet::extra_substitutions[type].
454 # file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> " + v
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)"
465 file
.puts
";; .yas-setup.el for #{modename} ends here"