├── Commands
├── Copy as RTF.tmCommand
├── Create CSS from Theme.plist
├── Create HTML from Document : Selection with Lines.plist
├── Create HTML from Document : Selection.plist
├── Insert Scratch Snippet.plist
├── Paste Selection : Line Online….tmCommand
├── Record Scratch Snippet.plist
└── Restart TextMate.plist
├── Preferences
├── Folding - Regular Expressions.tmPreferences
├── Properties Comments.tmPreferences
├── Properties Completions.tmPreferences
├── Smart Pairs (Regex Character Class).tmPreferences
├── Spell Checking: Disable for CamelCase Words.tmPreferences
└── Style: Separator.tmPreferences
├── README.mdown
├── Support
├── lib
│ ├── copy_as_rtf.rb
│ └── doctohtml.rb
└── nibs
│ └── pastebin.nib
│ ├── classes.nib
│ ├── info.nib
│ └── keyedobjects.nib
├── Syntaxes
├── FormatString.tmLanguage
├── Properties.tmLanguage
└── Regular Expressions (Oniguruma).tmLanguage
└── info.plist
/Commands/Copy as RTF.tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 |
10 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/copy_as_rtf.rb"
11 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb"
12 |
13 | doc = RtfExporter.new.generate_rtf( STDIN.read )
14 | `echo "hi" | pbcopy`
15 | Kernel.open('|pbcopy','w') do |f|
16 | f.write(doc)
17 | end
18 | print doc
19 |
20 | input
21 | selection
22 | inputFormat
23 | xml
24 | keyEquivalent
25 | ^~@c
26 | name
27 | Copy Document / Selection as RTF
28 | outputCaret
29 | afterOutput
30 | outputFormat
31 | text
32 | outputLocation
33 | discard
34 | uuid
35 | 6F9D791B-B8E5-456D-A574-1B5C71F232FF
36 | version
37 | 2
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Commands/Create CSS from Theme.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/doctohtml.rb"
10 | print generate_stylesheet_from_theme()
11 | input
12 | none
13 | inputFormat
14 | xml
15 | keyEquivalent
16 |
17 | name
18 | Create CSS From Current Theme
19 | output
20 | openAsNewDocument
21 | uuid
22 | ED204720-38FC-427C-B91E-D6AE866DAE3A
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Commands/Create HTML from Document : Selection with Lines.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/doctohtml.rb"
10 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb"
11 | unit = ENV.has_key?('TM_SELECTED_TEXT') ? 'selection' : 'document'
12 | TextMate.call_with_progress(:message => "Creating HTML version of #{unit}…") do
13 | print document_to_html( STDIN.read, { :line_numbers => true, :include_css => !ENV.has_key?('TM_SELECTED_TEXT') } )
14 | end
15 | input
16 | selection
17 | inputFormat
18 | xml
19 | name
20 | Create HTML From Document / Selection With Line Numbers
21 | output
22 | openAsNewDocument
23 | require
24 |
25 |
26 | name
27 | Themes
28 | uuid
29 | A4380B27-F366-4C70-A542-B00D26ED997E
30 |
31 |
32 | uuid
33 | 7AE6F783-F162-4063-850D-1441441849D8
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Commands/Create HTML from Document : Selection.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/doctohtml.rb"
10 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb"
11 | unit = ENV.has_key?('TM_SELECTED_TEXT') ? 'selection' : 'document'
12 | TextMate.call_with_progress(:message => "Creating HTML version of #{unit}…") do
13 | print document_to_html( STDIN.read, :include_css => !ENV.has_key?('TM_SELECTED_TEXT') )
14 | end
15 |
16 | input
17 | selection
18 | inputFormat
19 | xml
20 | name
21 | Create HTML From Document / Selection
22 | output
23 | openAsNewDocument
24 | require
25 |
26 |
27 | name
28 | Themes
29 | uuid
30 | A4380B27-F366-4C70-A542-B00D26ED997E
31 |
32 |
33 | uuid
34 | 950B3108-E2E3-414E-9C4C-EE068F59A895
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Commands/Insert Scratch Snippet.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | cat ${TMP:-/tmp}/TextMate-ScratchSnippet.txt
9 | input
10 | none
11 | keyEquivalent
12 | ^~s
13 | name
14 | Insert Scratch Snippet
15 | output
16 | insertAsSnippet
17 | uuid
18 | ADFED53B-16EC-4956-A6A7-3EA2B8140F86
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Commands/Paste Selection : Line Online….tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -KU
9 |
10 | BUNDLE_SUPPORT = ENV['TM_BUNDLE_SUPPORT']
11 | SUPPORT_PATH = ENV['TM_SUPPORT_PATH']
12 |
13 | PASTE_URL = ENV['TM_PASTIE_URL'] || 'http://pastie.org/pastes'
14 |
15 | require "#{BUNDLE_SUPPORT}/lib/doctohtml.rb"
16 | require "#{SUPPORT_PATH}/lib/textmate"
17 | require "#{SUPPORT_PATH}/lib/tm/detach"
18 | require "#{SUPPORT_PATH}/lib/progress"
19 | require "#{SUPPORT_PATH}/lib/escape"
20 | require 'cgi'
21 | require 'zlib'
22 | require "yaml"
23 | require "iconv"
24 | require "tempfile"
25 |
26 | PREFS_FILE = "#{ENV['HOME']}/Library/Preferences/com.macromates.textmate.paste_online.yaml"
27 |
28 | def load_prefs
29 | prefs = File.open(PREFS_FILE) { |file| YAML.load(file) } rescue { }
30 | selected = prefs['selectedDestinations'] || [[0]]
31 | return { 'selectedDestinations' => selected }
32 | end
33 |
34 | def save_prefs(params)
35 | selected = params['selectedDestinations'].dup
36 | File.open(PREFS_FILE, "w") { |file| YAML.dump({ 'selectedDestinations' => selected }, file) }
37 | end
38 |
39 | def get_destinations
40 | proclist = `ps -x -o command`
41 | active = []
42 |
43 | destinations = [{ 'name' => 'Open in Browser' }, { 'name' => 'Send to Clipboard' }]
44 | node = nil
45 |
46 | active = proclist.scan(%r{^(?:/.*\.app/Contents/MacOS/(Quicksilver)\b)}).flatten
47 |
48 | if active.include?('Quicksilver')
49 | destinations << { 'name' => 'Send to Quicksilver' }
50 | end
51 |
52 | prefs = load_prefs
53 |
54 | window_title = if ENV.has_key? 'TM_SELECTED_TEXT'; 'Paste Selection Online'; else 'Paste Document Online'; end
55 | default_wrap = (ENV['TM_SCOPE'] =~ /^text\./) ? 1 : 0
56 | parameters = {
57 | 'windowTitle' => window_title,
58 | 'destinations' => destinations,
59 | 'selectedDestinations' => prefs['selectedDestinations'],
60 | 'private' => true,
61 | 'lineWrap' => default_wrap,
62 | }.to_plist
63 |
64 | res = %x{ "$DIALOG" -cmp #{e_sh parameters} pastebin }
65 | exit if $? != 0
66 |
67 | res = OSX::PropertyList.load(res)
68 | exit unless res.has_key? 'returnCode'
69 |
70 | save_prefs(res)
71 |
72 | actions = []
73 | res['selectedDestinations'].to_a.each do |index_array|
74 | path = []
75 | node = destinations
76 | index_array.each do |index|
77 | path << node[index.to_i]['name']
78 | node = node[index.to_i]['children']
79 | end
80 | actions << path.join(' ')
81 | end
82 |
83 | [ actions.join("\n"), res['private'], res['lineWrap'] ]
84 | end
85 |
86 | TM_APP_PATH = TextMate.app_path
87 |
88 | def find_language_ext
89 | bundle_dirs = [
90 | File.expand_path('~/Library/Application Support/TextMate/Bundles'),
91 | '/Library/Application Support/TextMate/Bundles',
92 | TM_APP_PATH + '/Contents/SharedSupport/Bundles'
93 | ]
94 |
95 | if scope = ENV['TM_SCOPE'] then
96 | scope = scope.split(' ').first
97 | bundle_dirs.each do |dir|
98 | Dir.glob(dir + '/*.tmbundle/Syntaxes/*.{plist,tmLanguage}') do |filename|
99 | File.open(filename) do |io|
100 | begin
101 | plist = OSX::PropertyList.load(io)
102 | if scope == plist['scopeName'].to_s then
103 | return Array(plist['fileTypes']).first || 'txt'
104 | end
105 | rescue Exception => e
106 | abort "Error while parsing “#{filename}”\n\n#{e}"
107 | end
108 | end
109 | end
110 | end
111 | end
112 |
113 | ext = File.extname(ENV['TM_FILENAME'].to_s).sub(/\A\./, '')
114 | ext.empty? ? 'txt' : ext
115 | end
116 |
117 | def strip_leading(text, ch = ' ')
118 | count = text.gsub(/<[^>]+>/, '').split("\n").map { |e| e =~ /^#{ch}*(?!#{ch}|$)/ ? $&.length : nil }.reject { |e| e.nil? }.min
119 | text.send(text.respond_to?(:lines) ? :lines : :to_s).map { |line| count.times { line.sub!(/^((<[^>]+>)*)#{ch}/, '\1') }; line }.join
120 | end
121 |
122 | URL_REGEXP = %r{
123 | \A(https?://) # scheme
124 | ([0-9a-z_!~*\'()-]+\.)* # tertiary domain(s) -- e.g. www.
125 | ([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\. # second level domain
126 | ([a-z]{2,6}) # first level domain -- e.g. .com or .museum
127 | (:[0-9]{1,4})? # port number -- e.g. :80
128 | ((/?)| # a slash isn’t required if there is no file name
129 | (/[0-9a-z_!~*\'().;?:@&=+$,%#-]+)+/?)\z # path
130 | }xi
131 |
132 | def connect_to_server(name, url, *args)
133 | $status, res, response = :unknown, nil, '???'
134 |
135 | tmpFile = Tempfile.new('tm_paste_headers')
136 | tmpFilePath = tmpFile.path.dup
137 | tmpFile.close!
138 |
139 | server = $1 if url =~ %r{.*?://(.*?)(/.*)?$}
140 | TextMate.call_with_progress(:title => "Paste to #{name}", :message => "Contacting Host “#{server}”…", :cancel => lambda { $status = :cancel; Process.kill("INT", -Process.pid) }) do
141 | thr = Thread.new { sleep 60.0; $status = :timeout; Process.kill("INT", -Process.pid) rescue nil }
142 |
143 | oldpgid = Process.getpgid(0)
144 | Process.setpgid(0, 0)
145 | rd, wr = IO.pipe
146 | pid = fork do
147 | STDOUT.reopen(wr)
148 | rd.close
149 | exec('curl', url, "-D#{tmpFilePath}", '-sLo/dev/null', '-w%{url_effective}', *args)
150 | end
151 | Process.setpgid(0, oldpgid)
152 | wr.close
153 | res = rd.read
154 | Process.wait(pid)
155 |
156 | $status = :success if $?.exitstatus == 0 && $status == :unknown
157 |
158 | thr.kill unless $status == :timeout
159 | thr.join
160 | end
161 |
162 | response = $1 if File.read(tmpFilePath) =~ /\AHTTP\/[0-9.]+ (.*?)\r?\n/
163 | File.unlink(tmpFilePath)
164 |
165 | $status = :error if res !~ URL_REGEXP || res == url # pastie returns the initial URL on some errors
166 | return $status, res, response
167 | end
168 |
169 | def paste_stdin(priv, wrap)
170 | xml = STDIN.read
171 |
172 | text_file = Tempfile.new('tm_paste_text')
173 | html_file = Tempfile.new('tm_paste_html')
174 |
175 | text_file << CGI::unescapeHTML(xml.gsub(/<[^>]*>/, ''))
176 | text_file.close
177 |
178 | gz = Zlib::GzipWriter.new(html_file)
179 | gz.write(document_to_html(strip_leading(strip_leading(xml, " "), "\t")))
180 | gz.close
181 | html_file.close
182 |
183 | author = ENV['TM_AUTHOR'] || "#{ENV['TM_FULLNAME']} (#{ENV['USER']})"
184 | ext = find_language_ext
185 |
186 | start_time = Time.now
187 | $status, res, response = connect_to_server("Pastie", PASTE_URL,
188 | "-HExpect:",
189 | "-Fpaste[parser]=plaintext",
190 | "-Fpaste[restricted]=#{priv}",
191 | "-Fpaste[wrap]=#{wrap}",
192 | "-Fpaste[display_name]=#{author}",
193 | "-Fpaste[file_extension]=#{ext}",
194 | "-Fpaste[body]=<#{text_file.path}",
195 | "-Fpaste[textmate_html_gz]=<#{html_file.path}"
196 | )
197 |
198 | open(File.expand_path('~/Library/Logs/Pastie.log'), 'a') do |io|
199 | amount = (File.size(text_file.path) + File.size(html_file.path)).to_s.gsub(/\d{1,3}(?=(\d{3})+(?!\d))/, '\0,') + " bytes"
200 | time_elapsed = '%.1f seconds' % (Time.now - start_time)
201 | io << start_time.strftime('%F %T %Z: ') << response << " " <<
202 | case $status
203 | when :timeout then "Timeout pasting #{amount} after #{time_elapsed}\n"
204 | when :cancel then "User cancelled pasting of #{amount} after #{time_elapsed}\n"
205 | when :error then "Error after #{time_elapsed} pasting #{amount}\n"
206 | else "Pasted #{amount} in #{time_elapsed}: #{res}\n"
207 | end
208 | end
209 |
210 | case $status
211 | when :timeout then TextMate::UI.alert(:warning, "Timeout Pasting", "There was a problem pasting your text (timeout).", "OK")
212 | when :error then TextMate::UI.alert(:warning, "Error Pasting", "There was a problem pasting your text, pastie.org responded with #{response}", "OK")
213 | else false
214 | end
215 |
216 | return $status == :success ? res : nil
217 | end
218 |
219 | TextMate.detach {
220 | dests, priv, wrap = get_destinations
221 | unless dests.empty?
222 | if url = paste_stdin(priv, wrap)
223 | if dests.include?('Send to Clipboard')
224 | IO.popen('pbcopy', 'w') { |io| io << url }
225 | end
226 | if dests.include?('Open in Browser')
227 | %x{ open #{e_sh url} }
228 | end
229 | if dests.include?('Send to Quicksilver')
230 | %x{osascript <<'APPLESCRIPT'
231 | tell application "Quicksilver"
232 | activate
233 | set selection to "#{url}"
234 | end tell
235 | APPLESCRIPT}
236 | end
237 | end
238 | end
239 | }
240 |
241 | fallbackInput
242 | document
243 | input
244 | selection
245 | inputFormat
246 | xml
247 | keyEquivalent
248 | ^~V
249 | name
250 | Paste Document / Selection Online…
251 | outputCaret
252 | afterOutput
253 | outputFormat
254 | text
255 | outputLocation
256 | discard
257 | uuid
258 | 6E779E48-F146-49BF-B60C-EFDFD1380772
259 | version
260 | 2
261 |
262 |
263 |
--------------------------------------------------------------------------------
/Commands/Record Scratch Snippet.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | cat > ${TMP:-/tmp}/TextMate-ScratchSnippet.txt
9 | fallbackInput
10 | line
11 | input
12 | selection
13 | name
14 | Record Scratch Snippet
15 | output
16 | discard
17 | uuid
18 | 5F225755-5840-44CF-BC26-2D484DE833A0
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Commands/Restart TextMate.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/bin/sh
9 |
10 | { osascript -e "tell app \"$(basename "$TM_APP_PATH")\" to quit"
11 |
12 | while ps >/dev/null -xp "$PPID"; do
13 | if (( ++n == 10 )); then
14 | "$DIALOG" </dev/null alert --title "Relaunch Timed Out" --body "Unable to exit TextMate." --button1 OK
15 | exit
16 | fi
17 | sleep .2;
18 | done
19 |
20 | open "$TM_APP_PATH" --args -disableSessionRestore NO
21 |
22 | } &>/dev/null &
23 | input
24 | none
25 | keyEquivalent
26 | ^@q
27 | name
28 | Relaunch TextMate
29 | output
30 | discard
31 | uuid
32 | E5142394-B07A-11D9-8EC4-000D93589AF6
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Preferences/Folding - Regular Expressions.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Folding (Regular Expressions)
7 | scope
8 | source.regexp.oniguruma
9 | settings
10 |
11 | foldingStartMarker
12 | (/\*|\{|\()
13 | foldingStopMarker
14 | (\*/|\}|\))
15 |
16 | uuid
17 | A856C6FA-D67B-44F1-9440-052F86CB66E8
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Preferences/Properties Comments.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Properties: Comments
7 | scope
8 | source.tm-properties
9 | settings
10 |
11 | shellVariables
12 |
13 |
14 | name
15 | TM_COMMENT_START
16 | value
17 | #
18 |
19 |
20 |
21 | uuid
22 | AD495D39-E4CE-4790-8263-92851356C2F2
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Preferences/Properties Completions.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Properties: Completions
7 | scope
8 | source.tm-properties
9 | settings
10 |
11 | completions
12 |
13 | exclude
14 | excludeFiles
15 | excludeDirectories
16 | excludeInBrowser
17 | excludeInFolderSearch
18 | excludeInFileChooser
19 | excludeFilesInBrowser
20 | excludeDirectoriesInBrowser
21 | include
22 | includeFiles
23 | includeDirectories
24 | includeInBrowser
25 | includeInFileChooser
26 | includeFilesInBrowser
27 | includeDirectoriesInBrowser
28 | includeFilesInFileChooser
29 | projectDirectory
30 | windowTitle
31 | scopeAttributes
32 | theme
33 | fontName
34 | fontSize
35 | fileType
36 | binary
37 | showInvisibles
38 | tabSize
39 | softTabs
40 | spellChecking
41 | spellingLanguage
42 |
43 |
44 | uuid
45 | CE48C640-F525-4FFC-AADE-5EBB676D0705
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Preferences/Smart Pairs (Regex Character Class).tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Smart Typing Pairs (Regex Character Class)
7 | scope
8 | constant.other.character-class.set.regexp
9 | settings
10 |
11 | smartTypingPairs
12 |
13 |
14 | uuid
15 | 247A4984-203C-46ED-B973-82A7196A5C5C
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Preferences/Spell Checking: Disable for CamelCase Words.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Spell Checking: Disable for CamelCase Words
7 | scope
8 | meta.word.camel-case
9 | settings
10 |
11 | spellChecking
12 | 0
13 |
14 | uuid
15 | EAACA103-A5C9-4D33-BB23-BA908CB83F22
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Preferences/Style: Separator.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Style: Separator
7 | scope
8 | meta.separator
9 | settings
10 |
11 | background
12 | #3467D1
13 | foreground
14 | #FFFFFF
15 |
16 | uuid
17 | DA3A550A-F4AF-4CFD-935F-8687FA17DC4B
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.mdown:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | You can install this bundle in TextMate by opening the preferences and going to the bundles tab. After installation it will be automatically updated for you.
4 |
5 | # General
6 |
7 | * [Bundle Styleguide](http://kb.textmate.org/bundle_styleguide) — _before you make changes_
8 | * [Commit Styleguide](http://kb.textmate.org/commit_styleguide) — _before you send a pull request_
9 | * [Writing Bug Reports](http://kb.textmate.org/writing_bug_reports) — _before you report an issue_
10 |
11 | # License
12 |
13 | If not otherwise specified (see below), files in this repository fall under the following license:
14 |
15 | Permission to copy, use, modify, sell and distribute this
16 | software is granted. This software is provided "as is" without
17 | express or implied warranty, and with no claim as to its
18 | suitability for any purpose.
19 |
20 | An exception is made for files in readable text which contain their own license information, or files where an accompanying file exists (in the same directory) with a “-license” suffix added to the base-name name of the original file, and an extension of txt, html, or similar. For example “tidy” is accompanied by “tidy-license.txt”.
--------------------------------------------------------------------------------
/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
59 | require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist"
60 |
61 | unless theme_plist = load_theme
62 | print "Could not locate your theme file or it may be corrupt or unparsable!"
63 | abort
64 | end
65 |
66 | @font_name = `"$TM_QUERY" --setting fontName`.chomp || 'Menlo-Regular'
67 | @font_size = (`"$TM_QUERY" --setting fontSize`.chomp || 12).to_s
68 | @font_size.sub! /\.\d+$/, ''
69 | @font_size = @font_size.to_i * 2
70 |
71 | @font_name = '"' + @font_name + '"' if @font_name.include?(' ') &&
72 | !@font_name.include?('"')
73 |
74 | theme_plist['settings'].each do | setting |
75 | if (!setting['name'] and setting['settings'])
76 | body_fg = setting['settings']['foreground'] || '#000000'
77 | body_fg = hex_color_to_rtf(body_fg)
78 | @colors << body_fg
79 | @colors = body_fg + @colors
80 | next
81 | end
82 | if setting['name'] && setting['scope']
83 | scope_name = setting['scope']
84 | add_style_from_textmate_theme scope_name, setting['settings']
85 | end
86 | end
87 | end
88 |
89 | def color_table
90 | "{\\colortbl#{@colors}}"
91 | end
92 |
93 | def font_table
94 | "{\\fonttbl {\\f0 #{@font_name};}}"
95 | end
96 |
97 | def rtf_document input
98 | <<-RTF_DOC
99 | {\\rtf
100 | #{font_table}
101 | #{color_table}
102 | {\\pard\\ql
103 | \\f0\\fs#{@font_size} #{document_to_rtf input}
104 | }}
105 | RTF_DOC
106 | end
107 |
108 | # {\\rtf
109 | # #{font_table}
110 | # #{color_table}
111 | # {\\pard\\ql
112 | # \\f0\\fs#{@font_size}\\cf1\\b Hello, World!\\line
113 | # \\tab \\cf2 Next Line\\line
114 | # \\tab\\tab\\i \\cf3 Another line
115 | # }}
116 |
117 | def load_theme
118 | return OSX::PropertyList.load(File.open(ENV['TM_CURRENT_THEME_PATH']))
119 | end
120 |
121 | def detab(str, width)
122 | lines = str.split(/\n/)
123 | lines.each do | line |
124 | line_sans_markup = line.gsub(/<[^>]*>/, '').gsub(/&[^;]+;/i, '.')
125 | while (index = line_sans_markup.index("\t"))
126 | tab = line_sans_markup[0..index].jlength - 1
127 | padding = " " * ((tab / width + 1) * width - tab)
128 | line_sans_markup.sub!("\t", padding)
129 | line.sub!("\t", padding)
130 | end
131 | end
132 | return lines.join("\n")
133 | end
134 |
135 | def strip_leading(text, ch = ' ')
136 | count = text.gsub(/<[^>]+>/, '').split("\n").map { |e| e =~ /^#{ch}*(?!#{ch}|$)/ ? $&.length : nil }.reject { |e| e.nil? }.min
137 | text.send(text.respond_to?(:lines) ? :lines : :to_s).map { |line| count.times { line.sub!(/^((<[^>]+>)*)#{ch}/, '\1') }; line }.join
138 | end
139 |
140 | def document_to_rtf(input, opt = {})
141 | # Read the source document / selection
142 | # Convert tabs to spaces using configured tab width
143 | input = detab(input, (ENV['TM_TAB_SIZE'] || '2').to_i)
144 |
145 | input = strip_leading(input, " ")
146 |
147 | input.gsub! /\\/, "__backslash__"
148 | input.gsub! /\\n/, "__newline__"
149 | input.gsub! /\n/, "\\\\line\n"
150 | input.gsub! /\{/, "\\{"
151 | input.gsub! /\}/, "\\}"
152 | input.gsub! /__newline__/, "\\\\\\n"
153 | input.gsub! /__backslash__/, "\\\\\\"
154 |
155 |
156 | @style_stack = []
157 |
158 | # Meat. The poor-man's tokenizer. Fortunately, our input is simple
159 | # and easy to parse.
160 | tokens = input.split(/(<[^>]*>)/)
161 | code_rtf = ''
162 | tokens.each do |token|
163 | case token
164 | when /^<\//
165 | # closing tag
166 | pop_style
167 | when /^<>$/
168 | # skip empty tags, resulting from name = ''
169 | when /^
170 | if token =~ /^<([^>]+)>$/
171 | scope = $1
172 | push_style scope
173 | end
174 | else
175 | next if token.empty?
176 | code_rtf << '{'
177 | style = current_style_as_rtf
178 | if style && !style.empty? && !token.strip.empty?
179 | code_rtf << current_style_as_rtf << ' '
180 | end
181 | code_rtf << token << '}'
182 | end
183 | end
184 |
185 | return code_rtf
186 | end
187 |
188 | def current_style
189 | @style_stack[0] || {}
190 | end
191 |
192 | def get_style_recursive scopes, styles
193 | #scopes -= ["punctuation", "definition"] # nasty workaround hack
194 |
195 | return nil unless styles
196 | cur = scopes.shift.strip
197 |
198 | style = nil
199 | unless scopes.empty?
200 | style = get_style_recursive(scopes, styles[cur]) || style
201 | end
202 | style ||= styles[:default]
203 | end
204 |
205 | def current_style_as_rtf
206 | cur = current_style
207 | rtf = ''
208 | rtf << "\\cf#{cur[:color_index]}" if cur[:color_index]
209 | rtf << "\\b" if cur[:bold]
210 | rtf << "\\i" if cur[:italic]
211 | rtf << "\\ul" if cur[:underline]
212 | rtf
213 | end
214 |
215 | def push_style name
216 | cur = current_style
217 | new_style = get_style_recursive(name.split('.'), @styles)
218 | # p "current: #{cur.inspect}"
219 | new_style = cur.merge new_style if new_style
220 | new_style ||= cur || {}
221 | unless new_style[:color_index]
222 | new_style[:color_index] = 0
223 | end
224 | @style_stack.unshift new_style
225 | new_style
226 | end
227 |
228 | def pop_style
229 | @style_stack.shift
230 | end
231 |
232 | end
233 |
234 |
--------------------------------------------------------------------------------
/Support/lib/doctohtml.rb:
--------------------------------------------------------------------------------
1 | # By Brad Choate -- http://bradchoate.com/
2 |
3 | $: << ENV['TM_SUPPORT_PATH'] + '/lib'
4 | require "textmate"
5 |
6 | # Provides CSS-friendly variations of common Mac fonts that you
7 | # may use in TextMate. Feel free to edit these to your liking...
8 | FONT_MAP = {
9 | /\bcourier\b/i => 'Courier, "MS Courier New"',
10 | /\bbitstream.*mono\b/i => '"Bitstream Vera Sans Mono"',
11 | /\bandale\b/i => '"Andale Mono"',
12 | /\bDejaVuSansMono\b/i => '"DejaVu Sans Mono"'
13 | }
14 |
15 | def load_theme
16 | return OSX::PropertyList.load(File.open(ENV['TM_CURRENT_THEME_PATH']))
17 | end
18 |
19 | def to_rgba(color)
20 | colors = color.scan /^#(..)(..)(..)(..)/
21 | r = colors[0][0].hex
22 | g = colors[0][1].hex
23 | b = colors[0][2].hex
24 | a = colors[0][3].hex
25 | return "rgba(#{r}, #{g}, #{b}, #{ format '%0.02f', a / 255.0 })"
26 | end
27 |
28 | def generate_stylesheet_from_theme(theme_class = nil)
29 | theme_class = '' if theme_class == nil
30 | require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist"
31 |
32 | unless theme_plist = load_theme
33 | print "Could not locate your theme file!"
34 | abort
35 | end
36 |
37 | theme_comment = theme_plist['comment']
38 | theme_name = theme_plist['name']
39 | theme_class.replace(theme_name)
40 | theme_class.downcase!
41 | theme_class.gsub!(/[^a-z0-9_-]/, '_')
42 | theme_class.gsub!(/_+/, '_')
43 |
44 | font_name = `"$TM_QUERY" --setting fontName`.chomp || 'Menlo-Regular'
45 | font_size = (`"$TM_QUERY" --setting fontSize`.chomp || 12).to_s
46 | font_size.sub! /\.\d+$/, ''
47 |
48 | FONT_MAP.each do | font_re, font_alt |
49 | if (font_re.match(font_name))
50 | font_name = font_alt
51 | break
52 | end
53 | end
54 |
55 | font_name = '"' + font_name + '"' if font_name.include?(' ') &&
56 | !font_name.include?('"')
57 |
58 | theme_styles = ''
59 | body_fg = ''
60 | body_bg = ''
61 | selection_bg = ''
62 | pre_selector = "pre.textmate-source.#{theme_class}"
63 |
64 | theme_plist['settings'].each do | setting |
65 | if (!setting['name'] and setting['settings'])
66 | body_bg = setting['settings']['background'] || '#ffffff'
67 | body_fg = setting['settings']['foreground'] || '#000000'
68 | selection_bg = setting['settings']['selection']
69 | body_bg = to_rgba(body_bg) if body_bg =~ /#.{8}/
70 | body_fg = to_rgba(body_fg) if body_fg =~ /#.{8}/
71 | selection_bg = to_rgba(selection_bg) if selection_bg && selection_bg =~ /#.{8}/
72 | next
73 | end
74 | next unless setting['name'] and setting['scope']
75 | theme_styles << "/* " + setting['name'] + " */\n"
76 | scope_name = setting['scope']
77 | scope_name.gsub! /(^|[ ])-[^ ]+/, '' # strip negated scopes
78 | scope_name.gsub! /\./, '_' # change inner '.' to '_'
79 | scope_name.gsub! /(^|[ ])/, '\1.'
80 | scope_name.gsub! /(^|,\s+)/m, '\1' + pre_selector + ' '
81 | theme_styles << "#{scope_name} {\n"
82 | if (color = setting['settings']['foreground'])
83 | color = to_rgba(color) if color =~ /#.{8}/
84 | theme_styles << "\tcolor: " + color + ";\n"
85 | end
86 | if (style = setting['settings']['fontStyle'])
87 | theme_styles << "\tfont-style: italic;\n" if style =~ /\bitalic\b/i
88 | theme_styles << "\ttext-decoration: underline;\n" if style =~ /\bunderline\b/i
89 | theme_styles << "\tfont-weight: bold;\n" if style =~ /\bbold\b/i
90 | end
91 | if (color = setting['settings']['background'])
92 | color = to_rgba(color) if color =~ /#.{8}/
93 | theme_styles << "\tbackground-color: " + color + ";\n"
94 | end
95 | theme_styles << "}\n\n"
96 | end
97 |
98 | if (selection_bg)
99 | # currently, -moz-selection doesn't appear to support alpha transparency
100 | # so, i'm not assigning it until it does.
101 | selection_style = "#{pre_selector} ::selection {
102 | background-color: #{selection_bg};
103 | }"
104 | else
105 | selection_style = ""
106 | end
107 |
108 | return <]*>/, '').gsub(/&[^;]+;/i, '.')
159 | while (index = line_sans_markup.index("\t"))
160 | tab = line_sans_markup[0..index].jlength - 1
161 | padding = " " * ((tab / width + 1) * width - tab)
162 | line_sans_markup.sub!("\t", padding)
163 | line.sub!("\t", padding)
164 | end
165 | end
166 | return lines.join("\n")
167 | end
168 |
169 | def number(str)
170 | # number each line of input
171 | lines = str.split(/\n/)
172 | n = 0
173 | lines.each do | line |
174 | n += 1
175 | line.gsub!(/^(<\/span>)?/, "\\1#{ sprintf("%5d", n) } ")
176 | end
177 | return lines.join("\n")
178 | end
179 |
180 | def document_to_html(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'] || '8').to_i)
184 |
185 | html = ''
186 |
187 | theme_class = ''
188 | if opt[:include_css]
189 | # If you declare a 'http://...' link as a TM_SOURCE_STYLESHEET
190 | # shell variable, that will be used instead of generating a stylesheet
191 | # based on the current theme.
192 | if (ENV['TM_SOURCE_STYLESHEET'])
193 | styles = "\t\n"
194 | else
195 | styles = generate_stylesheet_from_theme(theme_class)
196 | end
197 |
198 | # Head block
199 | html = %{
200 |
201 |
202 | #{ ENV['TM_FILENAME'] || 'untitled' }
203 |
206 |
207 |
208 | }
209 | end
210 |
211 | # Meat. The poor-man's tokenizer. Fortunately, our input is simple
212 | # and easy to parse.
213 | tokens = input.split(/(<[^>]*>)/)
214 | code_html = ''
215 | tokens.each do |token|
216 | case token
217 | when /^<\//
218 | code_html << ""
219 | when /^<>$/
220 | # skip empty tags, resulting from name = ''
221 | when /^
222 | if token =~ /^<([^>]+)>$/
223 | classes = $1.split(/\./)
224 | list = []
225 | begin
226 | list.push(classes.join('_'))
227 | end while classes.pop
228 | code_html << ""
229 | end
230 | else
231 | code_html << token
232 | end
233 | end
234 |
235 | code_html = number(code_html) if opt[:line_numbers]
236 |
237 | html << "#{code_html}
"
240 |
241 | if opt[:include_css]
242 | # Closing
243 | html << "\n\n