Path: blob/master/elisp/emacs-for-python/yasnippet/extras/textmate_import.rb
990 views
#!/usr/bin/ruby1# -*- coding: utf-8 -*-2#!/usr/bin/env ruby3# -*- coding: utf-8 -*-4# textmate_import.rb --- import textmate snippets5#6# Copyright (C) 2009 Rob Christie, 2010 João Távora7#8# This is a quick script to generate YASnippets from TextMate Snippets.9#10# I based the script off of a python script of a similar nature by11# Jeff Wheeler: http://nokrev.com12# http://code.nokrev.com/?p=snippet-copier.git;a=blob_plain;f=snippet_copier.py13#14# Use textmate_import.rb --help to get usage information.1516require 'rubygems'17require 'plist'18require 'choice'19require 'fileutils'20require 'shellwords' # String#shellescape21require 'ruby-debug' if $DEBUG2223Choice.options do24header ''25header 'Standard Options:'2627option :bundle_dir do28short '-d'29long '--bundle-dir=PATH'30desc 'Tells the program the directory to find the TextMate bundle directory'31default '.'32end3334option :output_dir do35short '-o'36long '--output-dir=PATH'37desc 'What directory to write the new YASnippets to'38end3940option :snippet do41short '-f'42long '--file=SNIPPET FILE NAME'43desc 'A specific snippet that you want to copy or a glob for various files'44default '*.{tmSnippet,tmCommand,plist,tmMacro}'45end4647option :print_pretty do48short '-p'49long '--pretty-print'50desc 'Pretty prints multiple snippets when printing to standard out'51end5253option :quiet do54short '-q'55long '--quiet'56desc 'Be quiet.'57end5859option :convert_bindings do60short '-b'61long '--convert-bindings'62desc "TextMate \"keyEquivalent\" keys are translated to YASnippet \"# binding :\" directives"63end6465option :info_plist do66short '-g'67long '--info-plist=PLIST'68desc "Specify a plist file derive menu information from defaults to \"bundle-dir\"/info.plist"69end7071separator ''72separator 'Common options: '7374option :help do75long '--help'76desc 'Show this message'77end78end7980# Represents and is capable of outputting the representation of a81# TextMate menu in terms of `yas/define-menu'82#83class TmSubmenu8485@@excluded_items = [];86def self.excluded_items; @@excluded_items; end8788attr_reader :items, :name89def initialize(name, hash)90@items = hash["items"]91@name = name92end9394def to_lisp(allsubmenus,95deleteditems,96indent = 0,97thingy = ["(", ")"])9899first = true;100101string = ""102separator_useless = true;103items.each do |uuid|104if deleteditems.index(uuid)105$stderr.puts "#{uuid} has been deleted!"106next107end108string += "\n"109string += " " * indent110string += (first ? thingy[0] : (" " * thingy[0].length))111112submenu = allsubmenus[uuid]113snippet = TmSnippet::snippets_by_uid[uuid]114unimplemented = TmSnippet::unknown_substitutions["content"][uuid]115if submenu116str = "(yas/submenu "117string += str + "\"" + submenu.name + "\""118string += submenu.to_lisp(allsubmenus, deleteditems,119indent + str.length + thingy[0].length)120elsif snippet and not unimplemented121string += ";; " + snippet.name + "\n"122string += " " * (indent + thingy[0].length)123string += "(yas/item \"" + uuid + "\")"124separator_useless = false;125elsif snippet and unimplemented126string += ";; Ignoring " + snippet.name + "\n"127string += " " * (indent + thingy[0].length)128string += "(yas/ignore-item \"" + uuid + "\")"129separator_useless = true;130elsif (uuid =~ /---------------------/)131string += "(yas/separator)" unless separator_useless132end133first = false;134end135string += ")"136string += thingy[1]137138return string139end140141def self.main_menu_to_lisp (parsed_plist, modename)142mainmenu = parsed_plist["mainMenu"]143deleted = parsed_plist["deleted"]144145root = TmSubmenu.new("__main_menu__", mainmenu)146all = {}147148mainmenu["submenus"].each_pair do |k,v|149all[k] = TmSubmenu.new(v["name"], v)150end151152excluded = mainmenu["excludedItems"] + TmSubmenu::excluded_items153closing = "\n '("154closing+= excluded.collect do |uuid|155"\"" + uuid + "\""156end.join( "\n ") + "))"157158str = "(yas/define-menu "159return str + "'#{modename}" + root.to_lisp(all,160deleted,161str.length,162["'(" , closing])163end164end165166167# Represents a textmate snippet168#169# - @file is the .tmsnippet/.plist file path relative to cwd170#171# - optional @info is a Plist.parsed info.plist found in the bundle dir172#173# - @@snippets_by_uid is where one can find all the snippets parsed so174# far.175#176#177class SkipSnippet < RuntimeError; end178class TmSnippet179@@known_substitutions = {180"content" => {181"${TM_RAILS_TEMPLATE_START_RUBY_EXPR}" => "<%= ",182"${TM_RAILS_TEMPLATE_END_RUBY_EXPR}" => " %>",183"${TM_RAILS_TEMPLATE_START_RUBY_INLINE}" => "<% ",184"${TM_RAILS_TEMPLATE_END_RUBY_INLINE}" => " -%>",185"${TM_RAILS_TEMPLATE_END_RUBY_BLOCK}" => "end" ,186"${0:$TM_SELECTED_TEXT}" => "${0:`yas/selected-text`}",187/\$\{(\d+)\}/ => "$\\1",188"${1:$TM_SELECTED_TEXT}" => "${1:`yas/selected-text`}",189"${2:$TM_SELECTED_TEXT}" => "${2:`yas/selected-text`}",190'$TM_SELECTED_TEXT' => "`yas/selected-text`",191%r'\$\{TM_SELECTED_TEXT:([^\}]*)\}' => "`(or (yas/selected-text) \"\\1\")`",192%r'`[^`]+\n[^`]`' => Proc.new {|uuid, match| "(yas/multi-line-unknown " + uuid + ")"}},193"condition" => {194/^source\..*$/ => "" },195"binding" => {},196"type" => {}197}198199def self.extra_substitutions; @@extra_substitutions; end200@@extra_substitutions = {201"content" => {},202"condition" => {},203"binding" => {},204"type" => {}205}206207def self.unknown_substitutions; @@unknown_substitutions; end208@@unknown_substitutions = {209"content" => {},210"condition" => {},211"binding" => {},212"type" => {}213}214215@@snippets_by_uid={}216def self.snippets_by_uid; @@snippets_by_uid; end217218def initialize(file,info=nil)219@file = file220@info = info221@snippet = TmSnippet::read_plist(file)222@@snippets_by_uid[self.uuid] = self;223raise SkipSnippet.new "not a snippet/command/macro." unless (@snippet["scope"] || @snippet["command"])224raise SkipSnippet.new "looks like preferences."if @file =~ /Preferences\//225raise RuntimeError.new("Cannot convert this snippet #{file}!") unless @snippet;226end227228def name229@snippet["name"]230end231232def uuid233@snippet["uuid"]234end235236def key237@snippet["tabTrigger"]238end239240def condition241yas_directive "condition"242end243244def type245override = yas_directive "type"246if override247return override248else249return "# type: command\n" if @file =~ /(Commands\/|Macros\/)/250end251end252253def binding254yas_directive "binding"255end256257def content258known = @@known_substitutions["content"]259extra = @@extra_substitutions["content"]260if direct = extra[uuid]261return direct262else263ct = @snippet["content"]264if ct265known.each_pair do |k,v|266if v.respond_to? :call267ct.gsub!(k) {|match| v.call(uuid, match)}268else269ct.gsub!(k,v)270end271end272extra.each_pair do |k,v|273ct.gsub!(k,v)274end275# the remaining stuff is an unknown substitution276#277[ %r'\$\{ [^/\}\{:]* / [^/]* / [^/]* / [^\}]*\}'x ,278%r'\$\{[^\d][^}]+\}',279%r'`[^`]+`',280%r'\$TM_[\w_]+',281%r'\(yas/multi-line-unknown [^\)]*\)'282].each do |reg|283ct.scan(reg) do |match|284@@unknown_substitutions["content"][match] = self285end286end287return ct288else289@@unknown_substitutions["content"][uuid] = self290TmSubmenu::excluded_items.push(uuid)291return "(yas/unimplemented)"292end293end294end295296def to_yas297doc = "# -*- mode: snippet -*-\n"298doc << (self.type || "")299doc << "# uuid: #{self.uuid}\n"300doc << "# key: #{self.key}\n" if self.key301doc << "# contributor: Translated from textmate snippet by PROGRAM_NAME\n"302doc << "# name: #{self.name}\n"303doc << (self.binding || "")304doc << (self.condition || "")305doc << "# --\n"306doc << (self.content || "(yas/unimplemented)")307doc308end309310def self.canonicalize(filename)311invalid_char = /[^ a-z_0-9.+=~(){}\/'`&#,-]/i312313filename.314gsub(invalid_char, ''). # remove invalid characters315gsub(/ {2,}/,' '). # squeeze repeated spaces into a single one316rstrip # remove trailing whitespaces317end318319def yas_file()320File.join(TmSnippet::canonicalize(@file[0, @file.length-File.extname(@file).length]) + ".yasnippet")321end322323def self.read_plist(xml_or_binary)324begin325parsed = Plist::parse_xml(xml_or_binary)326return parsed if parsed327raise ArgumentError.new "Probably in binary format and parse_xml is very quiet..."328rescue StandardError => e329if (system "plutil -convert xml1 #{xml_or_binary.shellescape} -o /tmp/textmate_import.tmpxml")330return Plist::parse_xml("/tmp/textmate_import.tmpxml")331else332raise RuntimeError.new "plutil failed miserably, check if you have it..."333end334end335end336337private338339@@yas_to_tm_directives = {"condition" => "scope", "binding" => "keyEquivalent", "key" => "tabTrigger"}340def yas_directive(yas_directive)341#342# Merge "known" hardcoded substitution with "extra" substitutions343# provided in the .yas-setup.el file.344#345merged = @@known_substitutions[yas_directive].346merge(@@extra_substitutions[yas_directive])347#348# First look for an uuid-based direct substitution for this349# directive.350#351if direct = merged[uuid]352return "# #{yas_directive}: "+ direct + "\n" unless direct.empty?353else354tm_directive = @@yas_to_tm_directives[yas_directive]355val = tm_directive && @snippet[tm_directive]356if val and !val.delete(" ").empty? then357#358# Sort merged substitutions by length (bigger ones first,359# regexps last), and apply them to the value gotten for plist.360#361merged.sort_by do |what, with|362if what.respond_to? :length then -what.length else 0 end363end.each do |sub|364if val.gsub!(sub[0],sub[1])365return "# #{yas_directive}: "+ val + "\n" unless val.empty?366end367end368#369# If we get here, no substitution matched, so mark this an370# unknown substitution.371#372@@unknown_substitutions[yas_directive][val] = self373return "## #{yas_directive}: \""+ val + "\n"374end375end376end377378end379380381if $0 == __FILE__382# Read the the bundle's info.plist if can find it/guess it383#384info_plist_file = Choice.choices.info_plist || File.join(Choice.choices.bundle_dir,"info.plist")385info_plist = TmSnippet::read_plist(info_plist_file) if info_plist_file and File.readable? info_plist_file;386387# Calculate the mode name388#389modename = File.basename Choice.choices.output_dir || "major-mode-name"390391# Read in .yas-setup.el looking for the separator between auto-generated392#393original_dir = Dir.pwd394yas_setup_el_file = File.join(original_dir, Choice.choices.output_dir, ".yas-setup.el")395separator = ";; --**--"396whole, head , tail = "", "", ""397if File::exists? yas_setup_el_file398File.open yas_setup_el_file, 'r' do |file|399whole = file.read400head , tail = whole.split(separator)401end402else403head = ";; .yas-setup.el for #{modename}\n" + ";; \n"404end405406# Now iterate the tail part to find extra substitutions407#408tail ||= ""409head ||= ""410directive = nil411# puts "get this head #{head}"412head.each_line do |line|413case line414when /^;; Substitutions for:(.*)$/415directive = $~[1].strip416# puts "found the directove #{directive}"417when /^;;(.*)[ ]+=yyas>(.*)$/418replacewith = $~[2].strip419lookfor = $~[1]420lookfor.gsub!(/^[ ]*/, "")421lookfor.gsub!(/[ ]*$/, "")422# puts "found this wonderful substitution for #{directive} which is #{lookfor} => #{replacewith}"423unless !directive or replacewith =~ /yas\/unknown/ then424TmSnippet.extra_substitutions[directive][lookfor] = replacewith425end426end427end428429# Glob snippets into snippet_files, going into subdirs430#431Dir.chdir Choice.choices.bundle_dir432snippet_files_glob = File.join("**", Choice.choices.snippet)433snippet_files = Dir.glob(snippet_files_glob)434435# Attempt to convert each snippet files in snippet_files436#437puts "Will try to convert #{snippet_files.length} snippets...\n" unless Choice.choices.quiet438439440# Iterate the globbed files441#442snippet_files.each do |file|443begin444puts "Processing \"#{File.join(Choice.choices.bundle_dir,file)}\"\n" unless Choice.choices.quiet445snippet = TmSnippet.new(file,info_plist)446447if448file_to_create = File.join(original_dir, Choice.choices.output_dir, snippet.yas_file)449FileUtils.mkdir_p(File.dirname(file_to_create))450File.open(file_to_create, 'w') do |f|451f.write(snippet.to_yas)452end453else454if Choice.choices.print_pretty455puts "--------------------------------------------"456end457puts snippet.to_yas if Choice.choices.print_pretty or not Choice.choices.info_plist458if Choice.choices.print_pretty459puts "--------------------------------------------\n\n"460end461end462rescue SkipSnippet => e463$stdout.puts "Skipping \"#{file}\": #{e.message}"464rescue RuntimeError => e465$stderr.puts "Oops.... \"#{file}\": #{e.message}"466$strerr.puts "#{e.backtrace.join("\n")}" unless Choice.choices.quiet467end468end469470# Attempt to decypher the menu471#472menustr = TmSubmenu::main_menu_to_lisp(info_plist, modename) if info_plist473puts menustr if $DEBUG474475# Write some basic .yas-* files476#477if Choice.choices.output_dir478FileUtils.mkdir_p Choice.choices.output_dir479FileUtils.touch File.join(original_dir, Choice.choices.output_dir, ".yas-make-groups") unless menustr480FileUtils.touch File.join(original_dir, Choice.choices.output_dir, ".yas-ignore-filenames-as-triggers")481482# Now, output head + a new tail in (possibly new) .yas-setup.el483# file484#485File.open yas_setup_el_file, 'w' do |file|486file.puts head487file.puts separator488file.puts ";; Automatically generated code, do not edit this part"489file.puts ";; "490file.puts ";; Translated menu"491file.puts ";; "492file.puts menustr493file.puts494file.puts ";; Unknown substitutions"495file.puts ";; "496["content", "condition", "binding"].each do |type|497file.puts ";; Substitutions for: #{type}"498file.puts ";; "499# TmSnippet::extra_substitutions[type].500# each_pair do |k,v|501# file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> " + v502# end503unknown = TmSnippet::unknown_substitutions[type];504unknown.keys.uniq.each do |k|505file.puts ";; # as in " + unknown[k].yas_file506file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> (yas/unknown)"507file.puts ";; "508end509file.puts ";; "510file.puts511end512file.puts ";; .yas-setup.el for #{modename} ends here"513end514end515end516517518