diff options
Diffstat (limited to '')
-rwxr-xr-x | emacs.d/lisp/yasnippet/extras/textmate_import.rb | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/emacs.d/lisp/yasnippet/extras/textmate_import.rb b/emacs.d/lisp/yasnippet/extras/textmate_import.rb new file mode 100755 index 0000000..639e508 --- /dev/null +++ b/emacs.d/lisp/yasnippet/extras/textmate_import.rb @@ -0,0 +1,517 @@ +#!/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 |