├── Commands └── Copy as RTF.tmCommand ├── README.markdown ├── Support └── lib │ ├── copy_as_rtf.3 │ ├── copy_as_rtf.4 │ ├── copy_as_rtf.rb │ └── copy_as_rtf_2.rb └── info.plist /Commands/Copy as RTF.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | command 6 | #!/usr/bin/env ruby -rjcode -Ku 7 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/copy_as_rtf.rb" 8 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb" 9 | doc = RtfExporter.new.generate_rtf( STDIN.read ) 10 | `echo "hi" | pbcopy` 11 | Kernel.open('|pbcopy','w') do |f| 12 | f.write(doc) 13 | end 14 | print doc 15 | 16 | input 17 | selection 18 | inputFormat 19 | xml 20 | keyEquivalent 21 | ^~@r 22 | name 23 | Copy as RTF 24 | output 25 | discard 26 | uuid 27 | 6F9D791B-B8E5-456D-A574-1B5C71F232FF 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Copy as RTF TextMate bundle 2 | =========================== 3 | 4 | Need to copy + paste some text and _keep_ the syntax highlighting? 5 | 6 | Yes you do. All the time. For Keynote presentations. And probably other reasons. 7 | 8 | Install this bundle, and after selecting some pretty syntax highlighted text, 9 | use Ctrl+Alt+Cmd+R to copy it as RTF (rich text format) Now you can 10 | paste it directly into Keynote presentations. And other places where RTF is supported. 11 | 12 | 13 | Installation 14 | ============ 15 | 16 | mkdir -p ~/Library/Application\ Support/TextMate/Bundles 17 | cd ~/Library/Application\ Support/TextMate/Bundles 18 | git clone git://github.com/drnic/copy-as-rtf-tmbundle.git "Copy as RTF.tmbundle" 19 | 20 | Inside TextMate, click "Reload Bundles." 21 | 22 | Credit 23 | ====== 24 | 25 | This bundle was created by [Max Muermann](http://www.workingwithrails.com/person/8530-max-muermann) 26 | 27 | Its now hosted on github by [Dr Nic Williams](http://drnicwilliams.com) so it can live on. 28 | 29 | License 30 | ======= 31 | 32 | Don't know. Ask Max. -------------------------------------------------------------------------------- /Support/lib/copy_as_rtf.3: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | $: << ENV['TM_SUPPORT_PATH'] + '/lib' 4 | require "textmate" 5 | require 'cgi' 6 | 7 | class RtfExporter 8 | 9 | def initialize 10 | @styles={} 11 | @colors = "" 12 | @num_colors=1 13 | end 14 | 15 | def generate_rtf input 16 | generate_stylesheet_from_theme 17 | doc = rtf_document input 18 | CGI::unescapeHTML(doc) 19 | end 20 | 21 | def add_style_recursive scopes, style, styles 22 | current = scopes.shift.strip 23 | styles[current] ||= {} 24 | if scopes.empty? 25 | styles[current][:default] = style 26 | else 27 | add_style_recursive scopes, style, styles[current] 28 | end 29 | end 30 | 31 | def add_style_from_textmate_theme name, settings 32 | style = {} 33 | style_names = name.split ',' 34 | style_names.each do |sn| 35 | add_style_recursive sn.split('.'), style, @styles 36 | end 37 | 38 | if fs = settings['fontStyle'] 39 | style[:bold] = fs =~ /bold/ 40 | style[:italic] if fs =~ /italic/ 41 | style[:underline] if fs =~ /underline/ 42 | end 43 | if col = settings['foreground'] 44 | style[:color] = hex_color_to_rtf col 45 | @colors << style[:color] 46 | style[:color_index] = @num_colors+=1 47 | end 48 | end 49 | 50 | def hex_color_to_rtf hex 51 | hex =~ /#(..)(..)(..)/ 52 | r = $1.hex 53 | g = $2.hex 54 | b = $3.hex 55 | return "\\red#{r}\\green#{g}\\blue#{b};" 56 | end 57 | 58 | def generate_stylesheet_from_theme(theme_class = nil) 59 | theme_class = '' if theme_class == nil 60 | require "#{ENV['TM_SUPPORT_PATH']}/lib/plist" 61 | 62 | # Load TM preferences to discover the current theme and font settings 63 | textmate_pref_file = '~/Library/Preferences/com.macromates.textmate.plist' 64 | prefs = PropertyList.load(File.open(File.expand_path(textmate_pref_file))) 65 | theme_uuid = prefs['OakThemeManagerSelectedTheme'] 66 | # Load the active theme. Unfortunately, this requires us to scan through 67 | # all discoverable theme files... 68 | unless theme_plist = find_theme(theme_uuid) 69 | print "Could not locate your theme file!" 70 | abort 71 | end 72 | 73 | theme_comment = theme_plist['comment'] 74 | theme_name = theme_plist['name'] 75 | theme_class.replace(theme_name) 76 | theme_class.downcase! 77 | theme_class.gsub!(/[^a-z0-9_-]/, '_') 78 | theme_class.gsub!(/_+/, '_') 79 | 80 | @font_name = prefs['OakTextViewNormalFontName'] || 'Monaco' 81 | @font_size = (prefs['OakTextViewNormalFontSize'] || 11).to_s 82 | @font_size.sub! /\.\d+$/, '' 83 | @font_size = @font_size.to_i * 3 84 | 85 | @font_name = '"' + @font_name + '"' if @font_name.include?(' ') && 86 | !@font_name.include?('"') 87 | 88 | theme_plist['settings'].each do | setting | 89 | if (!setting['name'] and setting['settings']) 90 | body_bg = setting['settings']['background'] || '#ffffff' 91 | body_fg = setting['settings']['foreground'] || '#000000' 92 | selection_bg = setting['settings']['selection'] 93 | body_bg = hex_color_to_rtf(body_bg) 94 | body_fg = hex_color_to_rtf(body_fg) 95 | selection_bg = hex_color_to_rtf(selection_bg) if selection_bg 96 | @colors << body_fg 97 | next 98 | end 99 | if setting['name'] && setting['scope'] 100 | scope_name = setting['scope'] 101 | # scope_name.gsub! /(^|[ ])-[^ ]+/, '' # strip negated scopes 102 | # scope_name.gsub! /\./, '_' # change inner '.' to '_' 103 | # #scope_name.gsub! /(^|[ ])/, '\1.' 104 | # scope_name.gsub! /[ ]/, '_' # spaces to underscores 105 | # scope_name.gsub! /(^|,\s+)/m, '\1' 106 | add_style_from_textmate_theme scope_name, setting['settings'] 107 | end 108 | end 109 | end 110 | 111 | def color_table 112 | "{\\colortbl;#{@colors}}" 113 | end 114 | 115 | def font_table 116 | "{\\fonttbl {\\f0 #{@font_name};}}" 117 | end 118 | 119 | def rtf_document input 120 | <<-RTF_DOC 121 | {\\rtf 122 | #{font_table} 123 | #{color_table} 124 | {\\pard\\ql 125 | \\f0\\fs#{@font_size} #{document_to_rtf input} 126 | }} 127 | RTF_DOC 128 | end 129 | 130 | # {\\rtf 131 | # #{font_table} 132 | # #{color_table} 133 | # {\\pard\\ql 134 | # \\f0\\fs#{@font_size}\\cf1\\b Hello, World!\\line 135 | # \\tab \\cf2 Next Line\\line 136 | # \\tab\\tab\\i \\cf3 Another line 137 | # }} 138 | 139 | # Search heuristic is based on the Theme Builder bundle's 140 | # "Create for Current Language" command 141 | def find_theme(uuid) 142 | theme_dirs = [ 143 | File.expand_path('~/Library/Application Support/TextMate/Themes'), 144 | '/Library/Application Support/TextMate/Themes', 145 | TextMate.app_path + '/Contents/SharedSupport/Themes' 146 | ] 147 | 148 | theme_dirs.each do |theme_dir| 149 | if File.exists? theme_dir 150 | themes = Dir.entries(theme_dir).find_all { |theme| theme =~ /.+\.(tmTheme|plist)$/ } 151 | themes.each do |theme| 152 | plist = PropertyList.load(File.open("#{theme_dir}/#{theme}")) 153 | return plist if plist["uuid"] == uuid 154 | end 155 | end 156 | end 157 | return nil 158 | end 159 | 160 | def detab(str, width) 161 | lines = str.split(/\n/) 162 | lines.each do | line | 163 | line_sans_markup = line.gsub(/<[^>]*>/, '').gsub(/&[^;]+;/i, '.') 164 | while (index = line_sans_markup.index("\t")) 165 | tab = line_sans_markup[0..index].jlength - 1 166 | padding = " " * ((tab / width + 1) * width - tab) 167 | line_sans_markup.sub!("\t", padding) 168 | line.sub!("\t", padding) 169 | end 170 | end 171 | return lines.join("\n") 172 | end 173 | 174 | def document_to_rtf(input, opt = {}) 175 | # Read the source document / selection 176 | # Convert tabs to spaces using configured tab width 177 | input = detab(input, (ENV['TM_TAB_SIZE'] || '8').to_i) 178 | input = input.gsub /\n/,"\\\\line\n" 179 | 180 | @style_stack = [] 181 | 182 | # Meat. The poor-man's tokenizer. Fortunately, our input is simple 183 | # and easy to parse. 184 | tokens = input.split(/(<[^>]*>)/) 185 | code_rtf = '' 186 | tokens.each do |token| 187 | case token 188 | when /^<\// 189 | # closing tag 190 | pop_style 191 | when /^<>$/ 192 | # skip empty tags, resulting from name = '' 193 | when /^]+)>$/ 195 | scope = $1 196 | push_style scope 197 | end 198 | else 199 | next if token.empty? 200 | code_rtf << '{' 201 | style = current_style_as_rtf 202 | if style && !style.empty? && !token.strip.empty? 203 | code_rtf << current_style_as_rtf << ' ' 204 | end 205 | code_rtf << token << '}' 206 | end 207 | end 208 | 209 | return code_rtf 210 | end 211 | 212 | def current_style 213 | @style_stack[0] || {} 214 | end 215 | 216 | def get_style_recursive scopes, styles 217 | return nil unless styles 218 | cur = scopes.shift.strip 219 | style = nil 220 | unless scopes.empty? 221 | style = get_style_recursive(scopes, styles[cur]) || style 222 | end 223 | style = style || styles[:default] 224 | style 225 | end 226 | 227 | def current_style_as_rtf 228 | cur = current_style 229 | rtf = '' 230 | rtf << "\\cf#{cur[:color_index]}" if cur[:color_index] 231 | rtf << "\\b" if cur[:bold] 232 | rtf << "\\i" if cur[:italic] 233 | rtf << "\\ul" if cur[:underline] 234 | rtf 235 | end 236 | 237 | def push_style name 238 | cur = current_style 239 | # p "getting #{name}" 240 | new_style = get_style_recursive(name.split('.'), @styles) 241 | # p "got #{new_style.inspect}" 242 | # p "current: #{cur.inspect}" 243 | new_style = cur.merge new_style if new_style 244 | new_style ||= {} 245 | unless new_style[:color_index] 246 | new_style[:color_index] = 0 247 | end 248 | # p "merged: #{new_style.inspect}" 249 | @style_stack.unshift new_style 250 | new_style 251 | end 252 | 253 | def pop_style 254 | @style_stack.shift 255 | end 256 | 257 | end 258 | -------------------------------------------------------------------------------- /Support/lib/copy_as_rtf.4: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | $: << ENV['TM_SUPPORT_PATH'] + '/lib' 4 | require "textmate" 5 | require 'cgi' 6 | 7 | class RtfExporter 8 | 9 | def initialize 10 | @styles={} 11 | @colors = "" 12 | @num_colors=1 13 | end 14 | 15 | def generate_rtf input 16 | generate_stylesheet_from_theme 17 | doc = rtf_document input 18 | CGI::unescapeHTML(doc) 19 | end 20 | 21 | def add_style_recursive scopes, style, styles 22 | current = scopes.shift.strip 23 | styles[current] ||= {} 24 | if scopes.empty? 25 | styles[current][:default] = style 26 | else 27 | add_style_recursive scopes, style, styles[current] 28 | end 29 | end 30 | 31 | def add_style_from_textmate_theme name, settings 32 | style = {} 33 | style_names = name.split ',' 34 | style_names.each do |sn| 35 | add_style_recursive sn.split('.'), style, @styles 36 | end 37 | 38 | if fs = settings['fontStyle'] 39 | style[:bold] = fs =~ /bold/ 40 | style[:italic] if fs =~ /italic/ 41 | style[:underline] if fs =~ /underline/ 42 | end 43 | if col = settings['foreground'] 44 | style[:color] = hex_color_to_rtf col 45 | @colors << style[:color] 46 | style[:color_index] = @num_colors+=1 47 | end 48 | end 49 | 50 | def hex_color_to_rtf hex 51 | hex =~ /#(..)(..)(..)/ 52 | r = $1.hex 53 | g = $2.hex 54 | b = $3.hex 55 | return "\\red#{r}\\green#{g}\\blue#{b};" 56 | end 57 | 58 | def generate_stylesheet_from_theme(theme_class = nil) 59 | theme_class = '' if theme_class == nil 60 | require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist" 61 | 62 | # Load TM preferences to discover the current theme and font settings 63 | textmate_pref_file = '~/Library/Preferences/com.macromates.textmate.plist' 64 | prefs = PropertyList.load(File.open(File.expand_path(textmate_pref_file))) 65 | theme_uuid = prefs['OakThemeManagerSelectedTheme'] 66 | # Load the active theme. Unfortunately, this requires us to scan through 67 | # all discoverable theme files... 68 | unless theme_plist = find_theme(theme_uuid) 69 | print "Could not locate your theme file!" 70 | abort 71 | end 72 | 73 | theme_comment = theme_plist['comment'] 74 | theme_name = theme_plist['name'] 75 | theme_class.replace(theme_name) 76 | theme_class.downcase! 77 | theme_class.gsub!(/[^a-z0-9_-]/, '_') 78 | theme_class.gsub!(/_+/, '_') 79 | 80 | @font_name = prefs['OakTextViewNormalFontName'] || 'Monaco' 81 | @font_size = (prefs['OakTextViewNormalFontSize'] || 11).to_s 82 | @font_size.sub! /\.\d+$/, '' 83 | @font_size = @font_size.to_i * 3 84 | 85 | @font_name = '"' + @font_name + '"' if @font_name.include?(' ') && 86 | !@font_name.include?('"') 87 | 88 | theme_plist['settings'].each do | setting | 89 | if (!setting['name'] and setting['settings']) 90 | body_bg = setting['settings']['background'] || '#ffffff' 91 | body_fg = setting['settings']['foreground'] || '#000000' 92 | selection_bg = setting['settings']['selection'] 93 | body_bg = hex_color_to_rtf(body_bg) 94 | body_fg = hex_color_to_rtf(body_fg) 95 | selection_bg = hex_color_to_rtf(selection_bg) if selection_bg 96 | @colors << body_fg 97 | next 98 | end 99 | if setting['name'] && setting['scope'] 100 | scope_name = setting['scope'] 101 | # scope_name.gsub! /(^|[ ])-[^ ]+/, '' # strip negated scopes 102 | # scope_name.gsub! /\./, '_' # change inner '.' to '_' 103 | # #scope_name.gsub! /(^|[ ])/, '\1.' 104 | # scope_name.gsub! /[ ]/, '_' # spaces to underscores 105 | # scope_name.gsub! /(^|,\s+)/m, '\1' 106 | add_style_from_textmate_theme scope_name, setting['settings'] 107 | end 108 | end 109 | end 110 | 111 | def color_table 112 | "{\\colortbl;#{@colors}}" 113 | end 114 | 115 | def font_table 116 | "{\\fonttbl {\\f0 #{@font_name};}}" 117 | end 118 | 119 | def rtf_document input 120 | <<-RTF_DOC 121 | {\\rtf 122 | #{font_table} 123 | #{color_table} 124 | {\\pard\\ql 125 | \\f0\\fs#{@font_size} #{document_to_rtf input} 126 | }} 127 | RTF_DOC 128 | end 129 | 130 | # {\\rtf 131 | # #{font_table} 132 | # #{color_table} 133 | # {\\pard\\ql 134 | # \\f0\\fs#{@font_size}\\cf1\\b Hello, World!\\line 135 | # \\tab \\cf2 Next Line\\line 136 | # \\tab\\tab\\i \\cf3 Another line 137 | # }} 138 | 139 | # Search heuristic is based on the Theme Builder bundle's 140 | # "Create for Current Language" command 141 | def find_theme(uuid) 142 | theme_dirs = [ 143 | File.expand_path('~/Library/Application Support/TextMate/Themes'), 144 | '/Library/Application Support/TextMate/Themes', 145 | TextMate.app_path + '/Contents/SharedSupport/Themes' 146 | ] 147 | 148 | theme_dirs.each do |theme_dir| 149 | if File.exists? theme_dir 150 | themes = Dir.entries(theme_dir).find_all { |theme| theme =~ /.+\.(tmTheme|plist)$/ } 151 | themes.each do |theme| 152 | plist = PropertyList.load(File.open("#{theme_dir}/#{theme}")) 153 | return plist if plist["uuid"] == uuid 154 | end 155 | end 156 | end 157 | return nil 158 | end 159 | 160 | def detab(str, width) 161 | lines = str.split(/\n/) 162 | lines.each do | line | 163 | line_sans_markup = line.gsub(/<[^>]*>/, '').gsub(/&[^;]+;/i, '.') 164 | while (index = line_sans_markup.index("\t")) 165 | tab = line_sans_markup[0..index].jlength - 1 166 | padding = " " * ((tab / width + 1) * width - tab) 167 | line_sans_markup.sub!("\t", padding) 168 | line.sub!("\t", padding) 169 | end 170 | end 171 | return lines.join("\n") 172 | end 173 | 174 | def document_to_rtf(input, opt = {}) 175 | # Read the source document / selection 176 | # Convert tabs to spaces using configured tab width 177 | input = detab(input, (ENV['TM_TAB_SIZE'] || '8').to_i) 178 | 179 | input.gsub! /\\/, "__backslash__" 180 | input.gsub! /\\n/, "__newline__" 181 | input.gsub! /\n/, "\\\\line\n" 182 | input.gsub! /\{/, "\\{" 183 | input.gsub! /\}/, "\\}" 184 | input.gsub! /__newline__/, "\\\\\\n" 185 | input.gsub! /__backslash__/, "\\\\\\" 186 | 187 | 188 | @style_stack = [] 189 | 190 | # Meat. The poor-man's tokenizer. Fortunately, our input is simple 191 | # and easy to parse. 192 | tokens = input.split(/(<[^>]*>)/) 193 | code_rtf = '' 194 | tokens.each do |token| 195 | case token 196 | when /^<\// 197 | # closing tag 198 | pop_style 199 | when /^<>$/ 200 | # skip empty tags, resulting from name = '' 201 | when /^]+)>$/ 203 | scope = $1 204 | push_style scope 205 | end 206 | else 207 | next if token.empty? 208 | code_rtf << '{' 209 | style = current_style_as_rtf 210 | if style && !style.empty? && !token.strip.empty? 211 | code_rtf << current_style_as_rtf << ' ' 212 | end 213 | code_rtf << token << '}' 214 | end 215 | end 216 | 217 | return code_rtf 218 | end 219 | 220 | def current_style 221 | @style_stack[0] || {} 222 | end 223 | 224 | def get_style_recursive scopes, styles 225 | #scopes -= ["punctuation", "definition"] # nasty workaround hack 226 | 227 | return nil unless styles 228 | cur = scopes.shift.strip 229 | 230 | style = nil 231 | unless scopes.empty? 232 | style = get_style_recursive(scopes, styles[cur]) || style 233 | end 234 | style ||= styles[:default] 235 | end 236 | 237 | def current_style_as_rtf 238 | cur = current_style 239 | rtf = '' 240 | rtf << "\\cf#{cur[:color_index]}" if cur[:color_index] 241 | rtf << "\\b" if cur[:bold] 242 | rtf << "\\i" if cur[:italic] 243 | rtf << "\\ul" if cur[:underline] 244 | rtf 245 | end 246 | 247 | def push_style name 248 | cur = current_style 249 | # p "getting #{name}" 250 | new_style = get_style_recursive(name.split('.'), @styles) 251 | # p "got #{new_style.inspect}" 252 | # p "current: #{cur.inspect}" 253 | new_style = cur.merge new_style if new_style 254 | new_style ||= cur || {} 255 | unless new_style[:color_index] 256 | new_style[:color_index] = 0 257 | end 258 | # p "merged: #{new_style.inspect}" 259 | @style_stack.unshift new_style 260 | new_style 261 | end 262 | 263 | def pop_style 264 | @style_stack.shift 265 | end 266 | 267 | end 268 | -------------------------------------------------------------------------------- /Support/lib/copy_as_rtf.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | $: << ENV['TM_SUPPORT_PATH'] + '/lib' 4 | require "textmate" 5 | require 'cgi' 6 | 7 | class RtfExporter 8 | 9 | def initialize 10 | @styles={} 11 | @colors = "" 12 | @num_colors=1 13 | end 14 | 15 | def generate_rtf input 16 | generate_stylesheet_from_theme 17 | doc = rtf_document input 18 | CGI::unescapeHTML(doc) 19 | end 20 | 21 | def add_style_recursive scopes, style, styles 22 | current = scopes.shift.strip 23 | styles[current] ||= {} 24 | if scopes.empty? 25 | styles[current][:default] = style 26 | else 27 | add_style_recursive scopes, style, styles[current] 28 | end 29 | end 30 | 31 | def add_style_from_textmate_theme name, settings 32 | style = {} 33 | style_names = name.split ',' 34 | style_names.each do |sn| 35 | add_style_recursive sn.split('.'), style, @styles 36 | end 37 | 38 | if fs = settings['fontStyle'] 39 | style[:bold] = fs =~ /bold/ 40 | style[:italic] if fs =~ /italic/ 41 | style[:underline] if fs =~ /underline/ 42 | end 43 | if col = settings['foreground'] 44 | style[:color] = hex_color_to_rtf col 45 | @colors << style[:color] 46 | style[:color_index] = @num_colors+=1 47 | end 48 | end 49 | 50 | def hex_color_to_rtf hex 51 | hex =~ /#(..)(..)(..)/ 52 | r = $1.hex 53 | g = $2.hex 54 | b = $3.hex 55 | return "\\red#{r}\\green#{g}\\blue#{b};" 56 | end 57 | 58 | def generate_stylesheet_from_theme(theme_class = nil) 59 | theme_class = '' if theme_class == nil 60 | require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist" 61 | 62 | # Load TM preferences to discover the current theme and font settings 63 | textmate_pref_file = '~/Library/Preferences/com.macromates.textmate.plist' 64 | prefs = OSX::PropertyList.load(File.open(File.expand_path(textmate_pref_file))) 65 | theme_uuid = prefs['OakThemeManagerSelectedTheme'] 66 | # Load the active theme. Unfortunately, this requires us to scan through 67 | # all discoverable theme files... 68 | unless theme_plist = find_theme(theme_uuid) 69 | print "Could not locate your theme file or it may be corrupt or unparsable!" 70 | abort 71 | end 72 | 73 | theme_comment = theme_plist['comment'] 74 | theme_name = theme_plist['name'] 75 | theme_class.replace(theme_name) 76 | theme_class.downcase! 77 | theme_class.gsub!(/[^a-z0-9_-]/, '_') 78 | theme_class.gsub!(/_+/, '_') 79 | 80 | @font_name = prefs['OakTextViewNormalFontName'] || 'Monaco' 81 | @font_size = (prefs['OakTextViewNormalFontSize'] || 11).to_s 82 | @font_size.sub! /\.\d+$/, '' 83 | @font_size = @font_size.to_i * 3 84 | 85 | @font_name = '"' + @font_name + '"' if @font_name.include?(' ') && 86 | !@font_name.include?('"') 87 | 88 | 89 | theme_plist['settings'].each do | setting | 90 | if (!setting['name'] and setting['settings']) 91 | body_bg = setting['settings']['background'] || '#ffffff' 92 | @body_bg ||= body_bg 93 | body_fg = setting['settings']['foreground'] || '#000000' 94 | selection_bg = setting['settings']['selection'] 95 | body_bg = hex_color_to_rtf(body_bg) 96 | body_fg = hex_color_to_rtf(body_fg) 97 | selection_bg = hex_color_to_rtf(selection_bg) if selection_bg 98 | @colors << body_fg 99 | next 100 | end 101 | if setting['name'] && setting['scope'] 102 | scope_name = setting['scope'] 103 | # scope_name.gsub! /(^|[ ])-[^ ]+/, '' # strip negated scopes 104 | # scope_name.gsub! /\./, '_' # change inner '.' to '_' 105 | # #scope_name.gsub! /(^|[ ])/, '\1.' 106 | # scope_name.gsub! /[ ]/, '_' # spaces to underscores 107 | # scope_name.gsub! /(^|,\s+)/m, '\1' 108 | add_style_from_textmate_theme scope_name, setting['settings'] 109 | end 110 | end 111 | end 112 | 113 | def color_table 114 | "{\\colortbl;#{@colors}}" 115 | end 116 | 117 | def font_table 118 | "{\\fonttbl {\\f0 #{@font_name};}}" 119 | end 120 | 121 | def rtf_document input 122 | <<-RTF_DOC 123 | {\\rtf 124 | #{font_table} 125 | #{color_table} 126 | {\\pard\\ql 127 | \\f0\\fs#{@font_size} #{document_to_rtf input} 128 | }} 129 | RTF_DOC 130 | end 131 | 132 | # {\\rtf 133 | # #{font_table} 134 | # #{color_table} 135 | # {\\pard\\ql 136 | # \\f0\\fs#{@font_size}\\cf1\\b Hello, World!\\line 137 | # \\tab \\cf2 Next Line\\line 138 | # \\tab\\tab\\i \\cf3 Another line 139 | # }} 140 | 141 | # Search heuristic is based on the Theme Builder bundle's 142 | # "Create for Current Language" command 143 | def find_theme(uuid) 144 | theme_dirs = [ 145 | File.expand_path('~/Library/Application Support/TextMate/Themes'), 146 | '/Library/Application Support/TextMate/Themes', 147 | TextMate.app_path + '/Contents/SharedSupport/Themes' 148 | ] 149 | 150 | theme_dirs.each do |theme_dir| 151 | if File.exists? theme_dir 152 | themes = Dir.entries(theme_dir).find_all { |theme| theme =~ /.+\.(tmTheme|plist)$/ } 153 | themes.each do |theme| 154 | begin 155 | plist = OSX::PropertyList.load(File.open("#{theme_dir}/#{theme}")) 156 | return plist if plist["uuid"] == uuid 157 | rescue OSX::PropertyListError => e 158 | # puts "Error parsing theme #{theme_dir}/#{theme}" - e.g. GitHub.tmTheme has issues 159 | end 160 | end 161 | end 162 | end 163 | return nil 164 | end 165 | 166 | def detab(str, width) 167 | lines = str.split(/\n/) 168 | lines.each do | line | 169 | line_sans_markup = line.gsub(/<[^>]*>/, '').gsub(/&[^;]+;/i, '.') 170 | while (index = line_sans_markup.index("\t")) 171 | tab = line_sans_markup[0..index].jlength - 1 172 | padding = " " * ((tab / width + 1) * width - tab) 173 | line_sans_markup.sub!("\t", padding) 174 | line.sub!("\t", padding) 175 | end 176 | end 177 | return lines.join("\n") 178 | end 179 | 180 | def document_to_rtf(input, opt = {}) 181 | # Read the source document / selection 182 | # Convert tabs to spaces using configured tab width 183 | input = detab(input, (ENV['TM_TAB_SIZE'] || '2').to_i) 184 | 185 | input.gsub! /\\/, "__backslash__" 186 | input.gsub! /\\n/, "__newline__" 187 | input.gsub! /\n/, "\\\\line\n" 188 | input.gsub! /\{/, "\\{" 189 | input.gsub! /\}/, "\\}" 190 | input.gsub! /__newline__/, "\\\\\\n" 191 | input.gsub! /__backslash__/, "\\\\\\" 192 | 193 | 194 | @style_stack = [] 195 | 196 | # Meat. The poor-man's tokenizer. Fortunately, our input is simple 197 | # and easy to parse. 198 | tokens = input.split(/(<[^>]*>)/) 199 | code_rtf = '' 200 | tokens.each do |token| 201 | case token 202 | when /^<\// 203 | # closing tag 204 | pop_style 205 | when /^<>$/ 206 | # skip empty tags, resulting from name = '' 207 | when /^]+)>$/ 209 | scope = $1 210 | push_style scope 211 | end 212 | else 213 | next if token.empty? 214 | code_rtf << '{' 215 | style = current_style_as_rtf 216 | if style && !style.empty? && !token.strip.empty? 217 | code_rtf << current_style_as_rtf << ' ' 218 | end 219 | code_rtf << token << '}' 220 | end 221 | end 222 | 223 | return code_rtf 224 | end 225 | 226 | def current_style 227 | @style_stack[0] || {} 228 | end 229 | 230 | def get_style_recursive scopes, styles 231 | #scopes -= ["punctuation", "definition"] # nasty workaround hack 232 | 233 | return nil unless styles 234 | cur = scopes.shift.strip 235 | 236 | style = nil 237 | unless scopes.empty? 238 | style = get_style_recursive(scopes, styles[cur]) || style 239 | end 240 | style ||= styles[:default] 241 | end 242 | 243 | def current_style_as_rtf 244 | cur = current_style 245 | rtf = '' 246 | rtf << "\\cf#{cur[:color_index]}" if cur[:color_index] 247 | rtf << "\\b" if cur[:bold] 248 | rtf << "\\i" if cur[:italic] 249 | rtf << "\\ul" if cur[:underline] 250 | rtf 251 | end 252 | 253 | def push_style name 254 | cur = current_style 255 | new_style = get_style_recursive(name.split('.'), @styles) 256 | # p "current: #{cur.inspect}" 257 | new_style = cur.merge new_style if new_style 258 | new_style ||= cur || {} 259 | unless new_style[:color_index] 260 | #45 works for Sunburst theme; 0 for Eiffle or IDLE theme 261 | new_style[:color_index] = (@body_bg == '#000000') ? 45 : 0 262 | end 263 | @style_stack.unshift new_style 264 | new_style 265 | end 266 | 267 | def pop_style 268 | @style_stack.shift 269 | end 270 | 271 | end 272 | 273 | -------------------------------------------------------------------------------- /Support/lib/copy_as_rtf_2.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | $: << ENV['TM_SUPPORT_PATH'] + '/lib' 4 | require "textmate" 5 | 6 | class RtfExporter 7 | 8 | def initialize 9 | @styles={} 10 | @colors = "" 11 | @num_colors=1 12 | end 13 | 14 | def generate_rtf input 15 | generate_stylesheet_from_theme 16 | doc = rtf_document input 17 | CGI.unescapeHTML(doc) 18 | end 19 | 20 | def add_style_recursive scopes, style, styles 21 | current = scopes.shift.strip 22 | styles[current] ||= {} 23 | if scopes.empty? 24 | styles[current][:default] = style 25 | else 26 | add_style_recursive scopes, style, styles[current] 27 | end 28 | end 29 | 30 | def add_style_from_textmate_theme name, settings 31 | style = {} 32 | style_names = name.split ',' 33 | style_names.each do |sn| 34 | add_style_recursive sn.split('.'), style, @styles 35 | end 36 | 37 | if fs = settings['fontStyle'] 38 | style[:bold] = fs =~ /bold/ 39 | style[:italic] if fs =~ /italic/ 40 | style[:underline] if fs =~ /underline/ 41 | end 42 | if col = settings['foreground'] 43 | style[:color] = hex_color_to_rtf col 44 | @colors << style[:color] 45 | style[:color_index] = @num_colors+=1 46 | end 47 | if col = settings['background'] 48 | style[:bg_color] = hex_color_to_rtf col 49 | @colors << style[:bg_color] 50 | style[:bg_color_index] = @num_colors+=1 51 | end 52 | end 53 | 54 | def hex_color_to_rtf hex 55 | hex =~ /#(..)(..)(..)/ 56 | r = $1.hex 57 | g = $2.hex 58 | b = $3.hex 59 | return "\\red#{r}\\green#{g}\\blue#{b};" 60 | end 61 | 62 | def generate_stylesheet_from_theme(theme_class = nil) 63 | theme_class = '' if theme_class == nil 64 | require "#{ENV['TM_SUPPORT_PATH']}/lib/plist" 65 | 66 | # Load TM preferences to discover the current theme and font settings 67 | textmate_pref_file = '~/Library/Preferences/com.macromates.textmate.plist' 68 | prefs = PropertyList.load(File.open(File.expand_path(textmate_pref_file))) 69 | theme_uuid = prefs['OakThemeManagerSelectedTheme'] 70 | # Load the active theme. Unfortunately, this requires us to scan through 71 | # all discoverable theme files... 72 | unless theme_plist = find_theme(theme_uuid) 73 | print "Could not locate your theme file!" 74 | abort 75 | end 76 | 77 | theme_comment = theme_plist['comment'] 78 | theme_name = theme_plist['name'] 79 | theme_class.replace(theme_name) 80 | theme_class.downcase! 81 | theme_class.gsub!(/[^a-z0-9_-]/, '_') 82 | theme_class.gsub!(/_+/, '_') 83 | 84 | @font_name = prefs['OakTextViewNormalFontName'] || 'Monaco' 85 | @font_size = (prefs['OakTextViewNormalFontSize'] || 11).to_s 86 | @font_size.sub! /\.\d+$/, '' 87 | @font_size = @font_size.to_i * 3 88 | 89 | @font_name = '"' + @font_name + '"' if @font_name.include?(' ') && 90 | !@font_name.include?('"') 91 | 92 | theme_plist['settings'].each do | setting | 93 | if (!setting['name'] and setting['settings']) 94 | body_bg = setting['settings']['background'] || '#ffffff' 95 | body_fg = setting['settings']['foreground'] || '#000000' 96 | selection_bg = setting['settings']['selection'] 97 | body_bg = hex_color_to_rtf(body_bg) 98 | body_fg = hex_color_to_rtf(body_fg) 99 | selection_bg = hex_color_to_rtf(selection_bg) if selection_bg 100 | @colors << body_fg 101 | next 102 | end 103 | if setting['name'] && setting['scope'] 104 | scope_name = setting['scope'] 105 | add_style_from_textmate_theme scope_name, setting['settings'] 106 | end 107 | end 108 | end 109 | 110 | def color_table 111 | "{\\colortbl;#{@colors}}" 112 | end 113 | 114 | def font_table 115 | "{\\fonttbl {\\f0 #{@font_name};}}" 116 | end 117 | 118 | def rtf_document input 119 | <<-RTF_DOC 120 | {\\rtf 121 | #{font_table} 122 | #{color_table} 123 | {\\pard\\ql 124 | \\f0\\fs#{@font_size} #{document_to_rtf input} 125 | }} 126 | RTF_DOC 127 | end 128 | 129 | # {\\rtf 130 | # #{font_table} 131 | # #{color_table} 132 | # {\\pard\\ql 133 | # \\f0\\fs#{@font_size}\\cf1\\b Hello, World!\\line 134 | # \\tab \\cf2 Next Line\\line 135 | # \\tab\\tab\\i \\cf3 Another line 136 | # }} 137 | 138 | # Search heuristic is based on the Theme Builder bundle's 139 | # "Create for Current Language" command 140 | def find_theme(uuid) 141 | theme_dirs = [ 142 | File.expand_path('~/Library/Application Support/TextMate/Themes'), 143 | '/Library/Application Support/TextMate/Themes', 144 | TextMate.app_path + '/Contents/SharedSupport/Themes' 145 | ] 146 | 147 | theme_dirs.each do |theme_dir| 148 | if File.exists? theme_dir 149 | themes = Dir.entries(theme_dir).find_all { |theme| theme =~ /.+\.(tmTheme|plist)$/ } 150 | themes.each do |theme| 151 | plist = PropertyList.load(File.open("#{theme_dir}/#{theme}")) 152 | return plist if plist["uuid"] == uuid 153 | end 154 | end 155 | end 156 | return nil 157 | end 158 | 159 | def detab(str, width) 160 | lines = str.split(/\n/) 161 | lines.each do | line | 162 | line_sans_markup = line #.gsub(/<[^>]*>/, '').gsub(/&[^;]+;/i, '.') 163 | while (index = line_sans_markup.index("\t")) 164 | tab = line_sans_markup[0..index].jlength - 1 165 | padding = " " * ((tab / width + 1) * width - tab) 166 | line_sans_markup.sub!("\t", padding) 167 | line.sub!("\t", padding) 168 | end 169 | end 170 | return lines.join("\n") 171 | end 172 | 173 | def document_to_rtf(input, opt = {}) 174 | # Read the source document / selection 175 | # Convert tabs to spaces using configured tab width 176 | input = detab(input, (ENV['TM_TAB_SIZE'] || '8').to_i) 177 | input = input.gsub /\n/,"\\\\line\n" 178 | 179 | @style_stack = [] 180 | 181 | # Meat. The poor-man's tokenizer. Fortunately, our input is simple 182 | # and easy to parse. 183 | tokens = input.split(/(<[^>]*>)/) 184 | code_rtf = '' 185 | tokens.each do |token| 186 | case token 187 | when /^<\// 188 | # closing tag 189 | pop_style 190 | when /^<>$/ 191 | # skip empty tags, resulting from name = '' 192 | when /^]+)>$/ 194 | scope = $1 195 | push_style scope 196 | end 197 | else 198 | next if token.empty? 199 | code_rtf << '{' 200 | style = current_style_as_rtf 201 | if style && !style.empty? && !token.strip.empty? 202 | code_rtf << current_style_as_rtf << ' ' 203 | end 204 | code_rtf << token.gsub(/\\/,'\\\\') << '}' 205 | end 206 | end 207 | 208 | return code_rtf 209 | end 210 | 211 | def current_style 212 | @style_stack[0] || {} 213 | end 214 | 215 | def get_style_recursive scopes, styles 216 | return nil unless styles 217 | cur = scopes.shift.strip 218 | style = nil 219 | unless scopes.empty? 220 | style = get_style_recursive(scopes, styles[cur]) || style 221 | end 222 | style = style || styles[:default] 223 | style 224 | end 225 | 226 | def current_style_as_rtf 227 | cur = current_style 228 | rtf = '' 229 | rtf << "\\cf#{cur[:color_index]}" if cur[:color_index] 230 | rtf << "\\cb1" if cur[:bg_color_index] 231 | rtf << "\\b" if cur[:bold] 232 | rtf << "\\i" if cur[:italic] 233 | rtf << "\\ul" if cur[:underline] 234 | rtf 235 | end 236 | 237 | def push_style name 238 | cur = current_style 239 | # p "getting #{name}" 240 | new_style = get_style_recursive(name.split('.'), @styles) 241 | # p "got #{new_style.inspect}" 242 | # p "current: #{cur.inspect}" 243 | new_style = cur.merge new_style if new_style 244 | new_style ||= {} 245 | unless new_style[:color_index] 246 | new_style[:color_index] = 0 247 | end 248 | # p "merged: #{new_style.inspect}" 249 | @style_stack.unshift new_style 250 | new_style 251 | end 252 | 253 | def pop_style 254 | @style_stack.shift 255 | end 256 | 257 | end 258 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Copy as RTF 7 | uuid 8 | B95B1A14-F5E8-482F-BE3D-E3A8CF0E42AD 9 | 10 | 11 | --------------------------------------------------------------------------------