├── 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 /^
194 | if token =~ /^<([^>]+)>$/
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 /^
202 | if token =~ /^<([^>]+)>$/
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 /^
208 | if token =~ /^<([^>]+)>$/
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 /^
193 | if token =~ /^<([^>]+)>$/
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 |
--------------------------------------------------------------------------------