-
Notifications
You must be signed in to change notification settings - Fork 10
/
ox-clip.el
554 lines (461 loc) · 19.1 KB
/
ox-clip.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
;;; ox-clip.el --- Cross-platform formatted copying for org-mode
;; Copyright(C) 2016-2024 John Kitchin
;; Author: John Kitchin <[email protected]>
;; URL: https://github.com/jkitchin/ox-clip
;; Version: 0.3
;; Keywords: org-mode
;; Package-Requires: ((org "8.2") (htmlize "0"))
;; This file is not currently part of GNU Emacs.
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2, or (at
;; your option) any later version.
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program ; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;;
;; This module copies selected regions in org-mode as formatted text on the
;; clipboard that can be pasted into other applications. When not in org-mode,
;; the htmlize library is used instead.
;; For Windows the html-clip-w32.py script will be installed. It works pretty
;; well, but I noticed that the hyperlinks in the TOC to headings don't work,
;; and strike-through doesn't seem to work. I have no idea how to fix either
;; issue.
;; Mac OSX needs textutils and pbcopy, which should be part of the base install.
;; Linux needs a relatively modern xclip, preferrably a version of at least
;; 0.12. https://github.com/astrand/xclip
;; The main command is `ox-clip-formatted-copy' that should work across
;; Windows, Mac and Linux. By default, it copies as html.
;;
;; Note: Images/equations may not copy well in html. Use `ox-clip-image-to-clipboard' to
;; copy the image or latex equation at point to the clipboard as an image. The
;; default latex scale is too small for me, so the default size for this is set
;; to 3 in `ox-clip-default-latex-scale'. This overrides the settings in
;; `org-format-latex-options'.
(require 'htmlize)
;;; Code:
(defgroup ox-clip nil
"Customization group for ox-clip."
:tag "ox-clip"
:group 'org)
(defcustom ox-clip-w32-cmd
(format "python %s"
(expand-file-name
"html-clip-w32.py"
(file-name-directory (or load-file-name (locate-library "ox-clip")))))
"Usually an absolute path to html-clip-w32.py.
Could also be an alist of (target . cmd)"
:group 'ox-clip
:type '(choice string (list (cons string string))))
(defcustom ox-clip-osx-cmd
'(("default" . "textutil -inputencoding UTF-8 -stdin -format html -convert rtf -stdout | pbcopy")
;; This may work better on Chrome and Slack
("html" . "hexdump -ve '1/1 \"%.2x\"' | xargs printf \"set the clipboard to {text:\\\" \\\", «class HTML»:«data HTML%s»}\" | osascript -")
;; This may work better on GitHUB
("markdown" . "pandoc -f html -t markdown - | grep -v \"^:::\" | sed 's/{#.*}//g' | pbcopy"))
"Possible commands to copy formatted text on osX.
This can be a string, or an alist of (target . cmd)."
:group 'ox-clip
:type '(choice string (list (cons string string))))
(defcustom ox-clip-linux-cmd
"xclip -verbose -i \"%f\" -t text/html -selection clipboard"
"Command to copy formatted text on linux.
This can be a string, or an alist of (target . cmd). You must
include %f in hte command. It will be converted to a generated
temporary filename at run-time."
:group 'ox-clip
:type '(choice string (list (cons string string))))
(defvar ox-clip-w32-py "#!/usr/bin/env python
# Adapted from http://code.activestate.com/recipes/474121-getting-html-from-the-windows-clipboard/
# HtmlClipboard
# An interface to the \"HTML Format\" clipboard data format
__author__ = \"Phillip Piper (jppx1[at]bigfoot.com)\"
__date__ = \"2006-02-21\"
__version__ = \"0.1\"
import re
import win32clipboard
#---------------------------------------------------------------------------
# Convenience functions to do the most common operation
def HasHtml():
\"\"\"
Return True if there is a Html fragment in the clipboard..
\"\"\"
cb = HtmlClipboard()
return cb.HasHtmlFormat()
def GetHtml():
\"\"\"
Return the Html fragment from the clipboard or None if there is no Html in the clipboard.
\"\"\"
cb = HtmlClipboard()
if cb.HasHtmlFormat():
return cb.GetFragment()
else:
return None
def PutHtml(fragment):
\"\"\"
Put the given fragment into the clipboard.
Convenience function to do the most common operation
\"\"\"
cb = HtmlClipboard()
cb.PutFragment(fragment)
#---------------------------------------------------------------------------
class HtmlClipboard:
CF_HTML = None
MARKER_BLOCK_OUTPUT = \\
\"Version:1.0\\r\\n\" \\
\"StartHTML:%09d\\r\\n\" \\
\"EndHTML:%09d\\r\\n\" \\
\"StartFragment:%09d\\r\\n\" \\
\"EndFragment:%09d\\r\\n\" \\
\"StartSelection:%09d\\r\\n\" \\
\"EndSelection:%09d\\r\\n\" \\
\"SourceURL:%s\\r\\n\"
MARKER_BLOCK_EX = \\
\"Version:(\\S+)\\s+\" \\
\"StartHTML:(\\d+)\\s+\" \\
\"EndHTML:(\\d+)\\s+\" \\
\"StartFragment:(\\d+)\\s+\" \\
\"EndFragment:(\\d+)\\s+\" \\
\"StartSelection:(\\d+)\\s+\" \\
\"EndSelection:(\\d+)\\s+\" \\
\"SourceURL:(\\S+)\"
MARKER_BLOCK_EX_RE = re.compile(MARKER_BLOCK_EX)
MARKER_BLOCK = \
\"Version:(\\S+)\\s+\" \\
\"StartHTML:(\\d+)\\s+\" \\
\"EndHTML:(\\d+)\\s+\" \\
\"StartFragment:(\\d+)\\s+\" \\
\"EndFragment:(\\d+)\\s+\" \\
\"SourceURL:(\\S+)\"
MARKER_BLOCK_RE = re.compile(MARKER_BLOCK)
DEFAULT_HTML_BODY = \
\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0 Transitional//EN\\\">\" \\
\"<HTML><HEAD></HEAD><BODY><!--StartFragment-->%s<!--EndFragment--></BODY></HTML>\"
def __init__(self):
self.html = None
self.fragment = None
self.selection = None
self.source = None
self.htmlClipboardVersion = None
def GetCfHtml(self):
\"\"\"
Return the FORMATID of the HTML format
\"\"\"
if self.CF_HTML is None:
self.CF_HTML = win32clipboard.RegisterClipboardFormat(\"HTML Format\")
return self.CF_HTML
def GetAvailableFormats(self):
\"\"\"
Return a possibly empty list of formats available on the clipboard
\"\"\"
formats = []
try:
win32clipboard.OpenClipboard(0)
cf = win32clipboard.EnumClipboardFormats(0)
while (cf != 0):
formats.append(cf)
cf = win32clipboard.EnumClipboardFormats(cf)
finally:
win32clipboard.CloseClipboard()
return formats
def HasHtmlFormat(self):
\"\"\"
Return a boolean indicating if the clipboard has data in HTML format
\"\"\"
return (self.GetCfHtml() in self.GetAvailableFormats())
def GetFromClipboard(self):
\"\"\"
Read and decode the HTML from the clipboard
\"\"\"
try:
win32clipboard.OpenClipboard(0)
src = win32clipboard.GetClipboardData(self.GetCfHtml())
self.DecodeClipboardSource(src.decode('utf-8'))
finally:
win32clipboard.CloseClipboard()
def DecodeClipboardSource(self, src):
\"\"\"
Decode the given string to figure out the details of the HTML that's on the string
\"\"\"
# Try the extended format first (which has an explicit selection)
matches = self.MARKER_BLOCK_EX_RE.match(src)
if matches:
self.prefix = matches.group(0)
self.htmlClipboardVersion = matches.group(1)
self.html = src[int(matches.group(2)):int(matches.group(3))]
self.fragment = src[int(matches.group(4)):int(matches.group(5))]
self.selection = src[int(matches.group(6)):int(matches.group(7))]
self.source = matches.group(8)
else:
# Failing that, try the version without a selection
matches = self.MARKER_BLOCK_RE.match(src)
if matches:
self.prefix = matches.group(0)
self.htmlClipboardVersion = matches.group(1)
self.html = src[int(matches.group(2)):int(matches.group(3))]
self.fragment = src[int(matches.group(4)):int(matches.group(5))]
self.source = matches.group(6)
self.selection = self.fragment
def GetHtml(self, refresh=False):
\"\"\"
Return the entire Html document
\"\"\"
if not self.html or refresh:
self.GetFromClipboard()
return self.html
def GetFragment(self, refresh=False):
\"\"\"
Return the Html fragment. A fragment is well-formated HTML enclosing the selected text
\"\"\"
if not self.fragment or refresh:
self.GetFromClipboard()
return self.fragment
def GetSelection(self, refresh=False):
\"\"\"
Return the part of the HTML that was selected. It might not be well-formed.
\"\"\"
if not self.selection or refresh:
self.GetFromClipboard()
return self.selection
def GetSource(self, refresh=False):
\"\"\"
Return the URL of the source of this HTML
\"\"\"
if not self.selection or refresh:
self.GetFromClipboard()
return self.source
def PutFragment(self, fragment, selection=None, html=None, source=None):
\"\"\"
Put the given well-formed fragment of Html into the clipboard.
selection, if given, must be a literal string within fragment.
html, if given, must be a well-formed Html document that textually
contains fragment and its required markers.
\"\"\"
if selection is None:
selection = fragment
if html is None:
html = self.DEFAULT_HTML_BODY % fragment
if source is None:
source = \"\"
fragmentStart = html.index(fragment)
fragmentEnd = fragmentStart + len(fragment)
selectionStart = html.index(selection)
selectionEnd = selectionStart + len(selection)
self.PutToClipboard(html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source)
def PutToClipboard(self, html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source=\"None\"):
\"\"\"
Replace the Clipboard contents with the given html information.
\"\"\"
try:
win32clipboard.OpenClipboard(0)
win32clipboard.EmptyClipboard()
src = self.EncodeClipboardSource(html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source)
win32clipboard.SetClipboardData(self.GetCfHtml(), src.encode('utf-8'))
finally:
win32clipboard.CloseClipboard()
def EncodeClipboardSource(self, html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source):
\"\"\"
Join all our bits of information into a string formatted as per the HTML format specs.
\"\"\"
# How long is the prefix going to be?
dummyPrefix = self.MARKER_BLOCK_OUTPUT % (0, 0, 0, 0, 0, 0, source)
lenPrefix = len(dummyPrefix)
prefix = self.MARKER_BLOCK_OUTPUT % (lenPrefix, len(html)+lenPrefix,
fragmentStart+lenPrefix, fragmentEnd+lenPrefix,
selectionStart+lenPrefix, selectionEnd+lenPrefix,
source)
return (prefix + html)
def DumpHtml():
cb = HtmlClipboard()
print(\"GetAvailableFormats()=%s\" % str(cb.GetAvailableFormats()))
print(\"HasHtmlFormat()=%s\" % str(cb.HasHtmlFormat()))
if cb.HasHtmlFormat():
cb.GetFromClipboard()
print(\"prefix=>>>%s<<<END\" % cb.prefix)
print(\"htmlClipboardVersion=>>>%s<<<END\" % cb.htmlClipboardVersion)
print(\"GetSelection()=>>>%s<<<END\" % cb.GetSelection())
print(\"GetFragment()=>>>%s<<<END\" % cb.GetFragment())
print(\"GetHtml()=>>>%s<<<END\" % cb.GetHtml())
print(\"GetSource()=>>>%s<<<END\" % cb.GetSource())
if __name__ == '__main__':
import sys
data = sys.stdin.read()
PutHtml(data)
"
"Windows Python Script for copying formatted text.")
(defcustom ox-clip-default-latex-scale 3
"Default scale to use in `org-format-latex-options'.
Used when creating preview images for copying."
:group 'ox-clip
:type 'number)
;; Create the windows python script if needed.
(when (and (eq system-type 'windows-nt)
(not (file-exists-p (expand-file-name
"html-clip-w32.py"
(file-name-directory (or load-file-name (locate-library "ox-clip")))))))
(with-temp-file (expand-file-name
"html-clip-w32.py"
(file-name-directory (or load-file-name (locate-library "ox-clip"))))
(insert ox-clip-w32-py)))
;;;###autoload
(defun ox-clip-get-command (options)
"Get the command form OPTIONS.
OPTIONS is one of `ox-clip-w32-cmd', `ox-clip-osx-cmd', or
`ox-clip-linux-cmd'. Those may be a string, or a list of
candidates to choose from."
(if (stringp options)
options
(cdr (assoc (completing-read "Copy to: " options) options))))
;;;###autoload
(defun ox-clip-formatted-copy (r1 r2 &optional subtreep)
"Export the selected region to HTML and copy it to the clipboard.
R1 and R2 define the selected region.
If SUBTREEP (interactively, the prefix argument) is non-nil then
export the current `org-mode' subtree, including hidden content."
(interactive (list
;; This seems wonky, but it turns out you can get non-selected
;; regions from these when the region is not active. Using "rP"
;; leads to false selections imo. I think this is less
;; surprising.
(when (region-active-p) (region-beginning))
(when (region-active-p) (region-end))
current-prefix-arg))
;; Put a copy in the kill ring in case you want it in Emacs.
(if (null subtreep)
(copy-region-as-kill r1 r2)
(org-mark-subtree)
(org-copy-subtree))
(if (equal major-mode 'org-mode)
(save-window-excursion
(let* ((org-html-with-latex 'dvipng)
;; by default we only copy visible stuff, i.e. it should look like you see
;; but if you choose subtreep, we copy it all
(visible-only (not subtreep))
(buf (org-export-to-buffer 'html "*Formatted Copy*" nil subtreep visible-only t))
(html (with-current-buffer buf (buffer-string))))
(cond
((eq system-type 'windows-nt)
(with-current-buffer buf
(shell-command-on-region
(point-min)
(point-max)
(ox-clip-get-command ox-clip-w32-cmd))))
((eq system-type 'darwin)
(with-current-buffer buf
(shell-command-on-region
(point-min)
(point-max)
(ox-clip-get-command ox-clip-osx-cmd))))
((eq system-type 'gnu/linux)
;; For some reason shell-command on region does not work with xclip.
(let* ((tmpfile (make-temp-file "ox-clip-" nil ".html"
(with-current-buffer buf (buffer-string))))
(proc (apply
'start-process "ox-clip" "*ox-clip*"
(split-string-and-unquote
(format-spec
(ox-clip-get-command ox-clip-linux-cmd)
`((?f . ,tmpfile)))
" "))))
(set-process-query-on-exit-flag proc nil))))
(kill-buffer buf)))
;; Use htmlize when not in org-mode.
(let ((html (htmlize-region-for-paste r1 r2)))
(cond
((eq system-type 'windows-nt)
(with-temp-buffer
(insert html)
(shell-command-on-region
(point-min)
(point-max)
(ox-clip-get-command ox-clip-w32-cmd))))
((eq system-type 'darwin)
(with-temp-buffer
(insert html)
(shell-command-on-region
(point-min)
(point-max)
(ox-clip-get-command ox-clip-osx-cmd))))
((eq system-type 'gnu/linux)
(let* ((tmpfile (make-temp-file "ox-clip-" nil ".html" html))
(proc (apply
'start-process "ox-clip" "*ox-clip*"
(split-string-and-unquote
(format-spec
(ox-clip-get-command ox-clip-linux-cmd)
`((?f . ,tmpfile)))
" "))))
(set-process-query-on-exit-flag proc nil)))))))
;; * copy images / latex fragments to the clipboard
(defun ox-clip-ov-at ()
"Get overlay at point. A helper to avoid dependency on ov.el."
(car (overlays-at (point))))
;;;###autoload
(defun ox-clip-image-to-clipboard (&optional scale)
"Copy the image file or latex fragment at point to the clipboard as an image.
SCALE is a numerical
prefix (default=`ox-clip-default-latex-scale') that determines
the size of the latex image. It has no effect on other kinds of
images. Currently only works on Linux."
(interactive "P")
(let* ((el (org-element-context))
(image-file (cond
;; on a latex fragment
((eq 'latex-fragment (org-element-type el))
(when (ox-clip-ov-at) (org-latex-preview))
;; should be no image, so we rebuild one
(let ((current-scale (plist-get org-format-latex-options :scale))
ov display file relfile)
(plist-put org-format-latex-options :scale
(or scale ox-clip-default-latex-scale))
(org-latex-preview)
(plist-put org-format-latex-options :scale current-scale)
(setq ov (ox-clip-ov-at)
display (overlay-get ov 'display)
file (plist-get (cdr display) :file))
(file-relative-name file)))
;; At a link of an image
((and (eq 'link (org-element-type el))
(string= "file" (org-element-property :type el))
(string-match (cdr (assoc "file" org-html-inline-image-rules))
(org-element-property :path el)))
(file-relative-name (org-element-property :path el)))
;; At a link of an image (which is an attachment)
((and (eq 'link (org-element-type el))
(string= "attachment" (org-element-property :type el))
(string-match (cdr (assoc "file" org-html-inline-image-rules))
(org-element-property :path el)))
(file-relative-name (org-attach-expand (org-element-property :path el))))
;; at an overlay with a display that is an image
((and (ox-clip-ov-at)
(overlay-get (ox-clip-ov-at) 'display)
(plist-get (cdr (overlay-get (ox-clip-ov-at) 'display)) :file)
(string-match (cdr (assoc "file" org-html-inline-image-rules))
(plist-get (cdr (overlay-get (ox-clip-ov-at) 'display))
:file)))
(file-relative-name (plist-get (cdr (overlay-get (ox-clip-ov-at) 'display))
:file)))
;; not sure what else we can do here.
(t
nil))))
(when image-file
(cond
((eq system-type 'windows-nt)
(message "Not supported yet."))
((eq system-type 'darwin)
(do-applescript
(format "set the clipboard to POSIX file \"%s\"" (expand-file-name image-file))))
((eq system-type 'gnu/linux)
(call-process-shell-command
(format "xclip -selection clipboard -t image/%s -i %s"
(file-name-extension image-file)
image-file)))))
(message "Copied %s" image-file)))
(provide 'ox-clip)
;;; ox-clip.el ends here