#!/usr/bin/ruby # -*- coding: utf-8 -*- #!/usr/bin/env ruby # -*- coding: utf-8 -*- # textmate_import.rb --- import textmate snippets # # Copyright (C) 2009 Rob Christie, 2010 João Távora # # This is a quick script to generate YASnippets from TextMate Snippets. # # I based the script off of a python script of a similar nature by # Jeff Wheeler: http://nokrev.com # http://code.nokrev.com/?p=snippet-copier.git;a=blob_plain;f=snippet_copier.py # # Use textmate_import.rb --help to get usage information. require 'rubygems' require 'plist' require 'choice' require 'fileutils' require 'shellwords' # String#shellescape require 'ruby-debug' if $DEBUG Choice.options do header '' header 'Standard Options:' option :bundle_dir do short '-d' long '--bundle-dir=PATH' desc 'Tells the program the directory to find the TextMate bundle directory' default '.' end option :output_dir do short '-o' long '--output-dir=PATH' desc 'What directory to write the new YASnippets to' default './textmate_import' end option :snippet do short '-f' long '--file=SNIPPET FILE NAME' desc 'A specific snippet that you want to copy or a glob for various files' default '*.{tmSnippet,tmCommand,plist,tmMacro}' end option :print_pretty do short '-p' long '--pretty-print' desc 'Pretty prints multiple snippets when printing to standard out' end option :quiet do short '-q' long '--quiet' desc 'Be quiet.' end option :convert_bindings do short '-b' long '--convert-bindings' desc "TextMate \"keyEquivalent\" keys are translated to YASnippet \"# binding :\" directives" end option :info_plist do short '-g' long '--info-plist=PLIST' desc "Specify a plist file derive menu information from defaults to \"bundle-dir\"/info.plist" end separator '' separator 'Common options: ' option :help do long '--help' desc 'Show this message' end end # Represents and is capable of outputting the representation of a # TextMate menu in terms of `yas/define-menu' # class TmSubmenu @@excluded_items = []; def self.excluded_items; @@excluded_items; end attr_reader :items, :name def initialize(name, hash) @items = hash["items"] @name = name end def to_lisp(allsubmenus, deleteditems, indent = 0, thingy = ["(", ")"]) first = true; string = "" separator_useless = true; items.each do |uuid| if deleteditems.index(uuid) $stderr.puts "#{uuid} has been deleted!" next end string += "\n" string += " " * indent string += (first ? thingy[0] : (" " * thingy[0].length)) submenu = allsubmenus[uuid] snippet = TmSnippet::snippets_by_uid[uuid] unimplemented = TmSnippet::unknown_substitutions["content"][uuid] if submenu str = "(yas/submenu " string += str + "\"" + submenu.name + "\"" string += submenu.to_lisp(allsubmenus, deleteditems, indent + str.length + thingy[0].length) elsif snippet and not unimplemented string += ";; " + snippet.name + "\n" string += " " * (indent + thingy[0].length) string += "(yas/item \"" + uuid + "\")" separator_useless = false; elsif snippet and unimplemented string += ";; Ignoring " + snippet.name + "\n" string += " " * (indent + thingy[0].length) string += "(yas/ignore-item \"" + uuid + "\")" separator_useless = true; elsif (uuid =~ /---------------------/) string += "(yas/separator)" unless separator_useless end first = false; end string += ")" string += thingy[1] return string end def self.main_menu_to_lisp (parsed_plist, modename) mainmenu = parsed_plist["mainMenu"] deleted = parsed_plist["deleted"] root = TmSubmenu.new("__main_menu__", mainmenu) all = {} mainmenu["submenus"].each_pair do |k,v| all[k] = TmSubmenu.new(v["name"], v) end excluded = mainmenu["excludedItems"] + TmSubmenu::excluded_items closing = "\n '(" closing+= excluded.collect do |uuid| "\"" + uuid + "\"" end.join( "\n ") + "))" str = "(yas/define-menu " return str + "'#{modename}" + root.to_lisp(all, deleted, str.length, ["'(" , closing]) end end # Represents a textmate snippet # # - @file is the .tmsnippet/.plist file path relative to cwd # # - optional @info is a Plist.parsed info.plist found in the bundle dir # # - @@snippets_by_uid is where one can find all the snippets parsed so # far. # # class SkipSnippet < RuntimeError; end class TmSnippet @@known_substitutions = { "content" => { "${TM_RAILS_TEMPLATE_START_RUBY_EXPR}" => "<%= ", "${TM_RAILS_TEMPLATE_END_RUBY_EXPR}" => " %>", "${TM_RAILS_TEMPLATE_START_RUBY_INLINE}" => "<% ", "${TM_RAILS_TEMPLATE_END_RUBY_INLINE}" => " -%>", "${TM_RAILS_TEMPLATE_END_RUBY_BLOCK}" => "end" , "${0:$TM_SELECTED_TEXT}" => "${0:`yas/selected-text`}", /\$\{(\d+)\}/ => "$\\1", "${1:$TM_SELECTED_TEXT}" => "${1:`yas/selected-text`}", "${2:$TM_SELECTED_TEXT}" => "${2:`yas/selected-text`}", '$TM_SELECTED_TEXT' => "`yas/selected-text`", %r'\$\{TM_SELECTED_TEXT:([^\}]*)\}' => "`(or (yas/selected-text) \"\\1\")`", %r'`[^`]+\n[^`]`' => Proc.new {|uuid, match| "(yas/multi-line-unknown " + uuid + ")"}}, "condition" => { /^source\..*$/ => "" }, "binding" => {}, "type" => {} } def self.extra_substitutions; @@extra_substitutions; end @@extra_substitutions = { "content" => {}, "condition" => {}, "binding" => {}, "type" => {} } def self.unknown_substitutions; @@unknown_substitutions; end @@unknown_substitutions = { "content" => {}, "condition" => {}, "binding" => {}, "type" => {} } @@snippets_by_uid={} def self.snippets_by_uid; @@snippets_by_uid; end def initialize(file,info=nil) @file = file @info = info @snippet = TmSnippet::read_plist(file) @@snippets_by_uid[self.uuid] = self; raise SkipSnippet.new "not a snippet/command/macro." unless (@snippet["scope"] || @snippet["command"]) raise SkipSnippet.new "looks like preferences."if @file =~ /Preferences\// raise RuntimeError.new("Cannot convert this snippet #{file}!") unless @snippet; end def name @snippet["name"] end def uuid @snippet["uuid"] end def key @snippet["tabTrigger"] end def condition yas_directive "condition" end def type override = yas_directive "type" if override return override else return "# type: command\n" if @file =~ /(Commands\/|Macros\/)/ end end def binding yas_directive "binding" end def content known = @@known_substitutions["content"] extra = @@extra_substitutions["content"] if direct = extra[uuid] return direct else ct = @snippet["content"] if ct known.each_pair do |k,v| if v.respond_to? :call ct.gsub!(k) {|match| v.call(uuid, match)} else ct.gsub!(k,v) end end extra.each_pair do |k,v| ct.gsub!(k,v) end # the remaining stuff is an unknown substitution # [ %r'\$\{ [^/\}\{:]* / [^/]* / [^/]* / [^\}]*\}'x , %r'\$\{[^\d][^}]+\}', %r'`[^`]+`', %r'\$TM_[\w_]+', %r'\(yas/multi-line-unknown [^\)]*\)' ].each do |reg| ct.scan(reg) do |match| @@unknown_substitutions["content"][match] = self end end return ct else @@unknown_substitutions["content"][uuid] = self TmSubmenu::excluded_items.push(uuid) return "(yas/unimplemented)" end end end def to_yas doc = "# -*- mode: snippet -*-\n" doc << (self.type || "") doc << "# uuid: #{self.uuid}\n" doc << "# key: #{self.key}\n" if self.key doc << "# contributor: Translated from textmate snippet by PROGRAM_NAME\n" doc << "# name: #{self.name}\n" doc << (self.binding || "") doc << (self.condition || "") doc << "# --\n" doc << (self.content || "(yas/unimplemented)") doc end def self.canonicalize(filename) invalid_char = /[^ a-z_0-9.+=~(){}\/'`&#,-]/i filename. gsub(invalid_char, ''). # remove invalid characters gsub(/ {2,}/,' '). # squeeze repeated spaces into a single one rstrip # remove trailing whitespaces end def yas_file() File.join(TmSnippet::canonicalize(@file[0, @file.length-File.extname(@file).length]) + ".yasnippet") end def self.read_plist(xml_or_binary) begin parsed = Plist::parse_xml(xml_or_binary) return parsed if parsed raise ArgumentError.new "Probably in binary format and parse_xml is very quiet..." rescue StandardError => e if (system "plutil -convert xml1 #{xml_or_binary.shellescape} -o /tmp/textmate_import.tmpxml") return Plist::parse_xml("/tmp/textmate_import.tmpxml") else raise RuntimeError.new "plutil failed miserably, check if you have it..." end end end private @@yas_to_tm_directives = {"condition" => "scope", "binding" => "keyEquivalent", "key" => "tabTrigger"} def yas_directive(yas_directive) # # Merge "known" hardcoded substitution with "extra" substitutions # provided in the .yas-setup.el file. # merged = @@known_substitutions[yas_directive]. merge(@@extra_substitutions[yas_directive]) # # First look for an uuid-based direct substitution for this # directive. # if direct = merged[uuid] return "# #{yas_directive}: "+ direct + "\n" unless direct.empty? else tm_directive = @@yas_to_tm_directives[yas_directive] val = tm_directive && @snippet[tm_directive] if val and !val.delete(" ").empty? then # # Sort merged substitutions by length (bigger ones first, # regexps last), and apply them to the value gotten for plist. # merged.sort_by do |what, with| if what.respond_to? :length then -what.length else 0 end end.each do |sub| if val.gsub!(sub[0],sub[1]) return "# #{yas_directive}: "+ val + "\n" unless val.empty? end end # # If we get here, no substitution matched, so mark this an # unknown substitution. # @@unknown_substitutions[yas_directive][val] = self return "## #{yas_directive}: \""+ val + "\n" end end end end if __FILE__ == $PROGRAM_NAME # Read the the bundle's info.plist if can find it/guess it # info_plist_file = Choice.choices.info_plist || File.join(Choice.choices.bundle_dir,"info.plist") info_plist = TmSnippet::read_plist(info_plist_file) if info_plist_file and File.readable? info_plist_file; # Calculate the mode name # modename = File.basename Choice.choices.output_dir || "major-mode-name" # Read in .yas-setup.el looking for the separator between auto-generated # original_dir = Dir.pwd yas_setup_el_file = File.join(original_dir, Choice.choices.output_dir, ".yas-setup.el") separator = ";; --**--" whole, head , tail = "", "", "" if File::exists? yas_setup_el_file File.open yas_setup_el_file, 'r' do |file| whole = file.read head , tail = whole.split(separator) end else head = ";; .yas-setup.el for #{modename}\n" + ";; \n" end # Now iterate the tail part to find extra substitutions # tail ||= "" head ||= "" directive = nil # puts "get this head #{head}" head.each_line do |line| case line when /^;; Substitutions for:(.*)$/ directive = $~[1].strip # puts "found the directove #{directive}" when /^;;(.*)[ ]+=yyas>(.*)$/ replacewith = $~[2].strip lookfor = $~[1] lookfor.gsub!(/^[ ]*/, "") lookfor.gsub!(/[ ]*$/, "") # puts "found this wonderful substitution for #{directive} which is #{lookfor} => #{replacewith}" unless !directive or replacewith =~ /yas\/unknown/ then TmSnippet.extra_substitutions[directive][lookfor] = replacewith end end end # Glob snippets into snippet_files, going into subdirs # Dir.chdir Choice.choices.bundle_dir snippet_files_glob = File.join("**", Choice.choices.snippet) snippet_files = Dir.glob(snippet_files_glob) # Attempt to convert each snippet files in snippet_files # puts "Will try to convert #{snippet_files.length} snippets...\n" unless Choice.choices.quiet # Iterate the globbed files # snippet_files.each do |file| begin puts "Processing \"#{File.join(Choice.choices.bundle_dir,file)}\"\n" unless Choice.choices.quiet snippet = TmSnippet.new(file,info_plist) if file_to_create = File.join(original_dir, Choice.choices.output_dir, snippet.yas_file) FileUtils.mkdir_p(File.dirname(file_to_create)) File.open(file_to_create, 'w') do |f| f.write(snippet.to_yas) end else if Choice.choices.print_pretty puts "--------------------------------------------" end puts snippet.to_yas if Choice.choices.print_pretty or not Choice.choices.info_plist if Choice.choices.print_pretty puts "--------------------------------------------\n\n" end end rescue SkipSnippet => e $stdout.puts "Skipping \"#{file}\": #{e.message}" rescue RuntimeError => e $stderr.puts "Oops.... \"#{file}\": #{e.message}" $strerr.puts "#{e.backtrace.join("\n")}" unless Choice.choices.quiet end end # Attempt to decypher the menu # menustr = TmSubmenu::main_menu_to_lisp(info_plist, modename) if info_plist puts menustr if $DEBUG # Write some basic .yas-* files # if Choice.choices.output_dir FileUtils.mkdir_p Choice.choices.output_dir FileUtils.touch File.join(original_dir, Choice.choices.output_dir, ".yas-make-groups") unless menustr FileUtils.touch File.join(original_dir, Choice.choices.output_dir, ".yas-ignore-filenames-as-triggers") # Now, output head + a new tail in (possibly new) .yas-setup.el # file # File.open yas_setup_el_file, 'w' do |file| file.puts head file.puts separator file.puts ";; Automatically generated code, do not edit this part" file.puts ";; " file.puts ";; Translated menu" file.puts ";; " file.puts menustr file.puts file.puts ";; Unknown substitutions" file.puts ";; " ["content", "condition", "binding"].each do |type| file.puts ";; Substitutions for: #{type}" file.puts ";; " # TmSnippet::extra_substitutions[type]. # each_pair do |k,v| # file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> " + v # end unknown = TmSnippet::unknown_substitutions[type]; unknown.keys.uniq.each do |k| file.puts ";; # as in " + unknown[k].yas_file file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> (yas/unknown)" file.puts ";; " end file.puts ";; " file.puts end file.puts ";; .yas-setup.el for #{modename} ends here" end end end