├── README.org └── ox-clip.el /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: ox-clip - Cross-platform Formatted copy commands for org-mode 2 | #+AUTHOR: John Kitchin 3 | #+DATE: August 5, 2017 4 | 5 | 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. 6 | 7 | For Windows the html-clip-w32.py script will be installed in the ox-clip install directory. 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. 8 | 9 | Mac OSX needs textutils and pbcopy, which should be part of the standard utilities available on MacOSX. 10 | 11 | Linux needs a relatively modern xclip. https://github.com/astrand/xclip 12 | 13 | There is one command: =ox-clip-formatted-copy= that should work across Windows, Mac and Linux. I recommend you bind this to a key. I like H-k (Hyper-k). 14 | 15 | Note: This file was extracted from https://github.com/jkitchin/scimax to make a smaller repo for MELPA (see https://github.com/jkitchin/scimax/issues/21). I extracted it using this git wizardry from: http://www.pixelite.co.nz/article/extracting-file-folder-from-git-repository-with-full-git-history/. This seems to have retained the history on the file modifications. 16 | 17 | 18 | #+BEGIN_SRC sh 19 | cd /path/to/scimax 20 | git log --pretty=email --patch-with-stat --reverse --full-index --binary -- ox-clip.el > /tmp/patch 21 | 22 | cd ~/tmp 23 | mkdir ox-clip 24 | git init 25 | git am < /tmp/patch 26 | git remote add origin git@github.com:jkitchin/ox-clip.git 27 | git push -f origin master 28 | #+END_SRC 29 | -------------------------------------------------------------------------------- /ox-clip.el: -------------------------------------------------------------------------------- 1 | ;;; ox-clip.el --- Cross-platform formatted copying for org-mode 2 | 3 | ;; Copyright(C) 2016-2024 John Kitchin 4 | 5 | ;; Author: John Kitchin 6 | ;; URL: https://github.com/jkitchin/ox-clip 7 | ;; Version: 0.3 8 | ;; Keywords: org-mode 9 | ;; Package-Requires: ((org "8.2") (htmlize "0")) 10 | 11 | ;; This file is not currently part of GNU Emacs. 12 | 13 | ;; This program is free software; you can redistribute it and/or 14 | ;; modify it under the terms of the GNU General Public License as 15 | ;; published by the Free Software Foundation; either version 2, or (at 16 | ;; your option) any later version. 17 | 18 | ;; This program is distributed in the hope that it will be useful, but 19 | ;; WITHOUT ANY WARRANTY; without even the implied warranty of 20 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21 | ;; General Public License for more details. 22 | 23 | ;; You should have received a copy of the GNU General Public License 24 | ;; along with this program ; see the file COPYING. If not, write to 25 | ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 26 | ;; Boston, MA 02111-1307, USA. 27 | 28 | ;;; Commentary: 29 | ;; 30 | ;; This module copies selected regions in org-mode as formatted text on the 31 | ;; clipboard that can be pasted into other applications. When not in org-mode, 32 | ;; the htmlize library is used instead. 33 | 34 | ;; For Windows the html-clip-w32.py script will be installed. It works pretty 35 | ;; well, but I noticed that the hyperlinks in the TOC to headings don't work, 36 | ;; and strike-through doesn't seem to work. I have no idea how to fix either 37 | ;; issue. 38 | 39 | ;; Mac OSX needs textutils and pbcopy, which should be part of the base install. 40 | 41 | ;; Linux needs a relatively modern xclip, preferrably a version of at least 42 | ;; 0.12. https://github.com/astrand/xclip 43 | 44 | ;; The main command is `ox-clip-formatted-copy' that should work across 45 | ;; Windows, Mac and Linux. By default, it copies as html. 46 | ;; 47 | ;; Note: Images/equations may not copy well in html. Use `ox-clip-image-to-clipboard' to 48 | ;; copy the image or latex equation at point to the clipboard as an image. The 49 | ;; default latex scale is too small for me, so the default size for this is set 50 | ;; to 3 in `ox-clip-default-latex-scale'. This overrides the settings in 51 | ;; `org-format-latex-options'. 52 | 53 | (require 'htmlize) 54 | 55 | ;;; Code: 56 | (defgroup ox-clip nil 57 | "Customization group for ox-clip." 58 | :tag "ox-clip" 59 | :group 'org) 60 | 61 | 62 | (defcustom ox-clip-w32-cmd 63 | (format "python %s" 64 | (expand-file-name 65 | "html-clip-w32.py" 66 | (file-name-directory (or load-file-name (locate-library "ox-clip"))))) 67 | "Usually an absolute path to html-clip-w32.py. 68 | Could also be an alist of (target . cmd)" 69 | :group 'ox-clip 70 | :type '(choice string (list (cons string string)))) 71 | 72 | 73 | (defcustom ox-clip-osx-cmd 74 | '(("default" . "textutil -inputencoding UTF-8 -stdin -format html -convert rtf -stdout | pbcopy") 75 | ;; This may work better on Chrome and Slack 76 | ("html" . "hexdump -ve '1/1 \"%.2x\"' | xargs printf \"set the clipboard to {text:\\\" \\\", «class HTML»:«data HTML%s»}\" | osascript -") 77 | ;; This may work better on GitHUB 78 | ("markdown" . "pandoc -f html -t markdown - | grep -v \"^:::\" | sed 's/{#.*}//g' | pbcopy")) 79 | "Possible commands to copy formatted text on osX. 80 | This can be a string, or an alist of (target . cmd)." 81 | :group 'ox-clip 82 | :type '(choice string (list (cons string string)))) 83 | 84 | 85 | (defcustom ox-clip-linux-cmd 86 | "xclip -verbose -i \"%f\" -t text/html -selection clipboard" 87 | "Command to copy formatted text on linux. 88 | This can be a string, or an alist of (target . cmd). You must 89 | include %f in hte command. It will be converted to a generated 90 | temporary filename at run-time." 91 | :group 'ox-clip 92 | :type '(choice string (list (cons string string)))) 93 | 94 | 95 | (defvar ox-clip-w32-py "#!/usr/bin/env python 96 | # Adapted from http://code.activestate.com/recipes/474121-getting-html-from-the-windows-clipboard/ 97 | # HtmlClipboard 98 | # An interface to the \"HTML Format\" clipboard data format 99 | 100 | __author__ = \"Phillip Piper (jppx1[at]bigfoot.com)\" 101 | __date__ = \"2006-02-21\" 102 | __version__ = \"0.1\" 103 | 104 | import re 105 | import win32clipboard 106 | 107 | #--------------------------------------------------------------------------- 108 | # Convenience functions to do the most common operation 109 | 110 | def HasHtml(): 111 | \"\"\" 112 | Return True if there is a Html fragment in the clipboard.. 113 | \"\"\" 114 | cb = HtmlClipboard() 115 | return cb.HasHtmlFormat() 116 | 117 | 118 | def GetHtml(): 119 | \"\"\" 120 | Return the Html fragment from the clipboard or None if there is no Html in the clipboard. 121 | \"\"\" 122 | cb = HtmlClipboard() 123 | if cb.HasHtmlFormat(): 124 | return cb.GetFragment() 125 | else: 126 | return None 127 | 128 | 129 | def PutHtml(fragment): 130 | \"\"\" 131 | Put the given fragment into the clipboard. 132 | Convenience function to do the most common operation 133 | \"\"\" 134 | cb = HtmlClipboard() 135 | cb.PutFragment(fragment) 136 | 137 | 138 | #--------------------------------------------------------------------------- 139 | 140 | class HtmlClipboard: 141 | 142 | CF_HTML = None 143 | 144 | MARKER_BLOCK_OUTPUT = \\ 145 | \"Version:1.0\\r\\n\" \\ 146 | \"StartHTML:%09d\\r\\n\" \\ 147 | \"EndHTML:%09d\\r\\n\" \\ 148 | \"StartFragment:%09d\\r\\n\" \\ 149 | \"EndFragment:%09d\\r\\n\" \\ 150 | \"StartSelection:%09d\\r\\n\" \\ 151 | \"EndSelection:%09d\\r\\n\" \\ 152 | \"SourceURL:%s\\r\\n\" 153 | 154 | MARKER_BLOCK_EX = \\ 155 | \"Version:(\\S+)\\s+\" \\ 156 | \"StartHTML:(\\d+)\\s+\" \\ 157 | \"EndHTML:(\\d+)\\s+\" \\ 158 | \"StartFragment:(\\d+)\\s+\" \\ 159 | \"EndFragment:(\\d+)\\s+\" \\ 160 | \"StartSelection:(\\d+)\\s+\" \\ 161 | \"EndSelection:(\\d+)\\s+\" \\ 162 | \"SourceURL:(\\S+)\" 163 | MARKER_BLOCK_EX_RE = re.compile(MARKER_BLOCK_EX) 164 | 165 | MARKER_BLOCK = \ 166 | \"Version:(\\S+)\\s+\" \\ 167 | \"StartHTML:(\\d+)\\s+\" \\ 168 | \"EndHTML:(\\d+)\\s+\" \\ 169 | \"StartFragment:(\\d+)\\s+\" \\ 170 | \"EndFragment:(\\d+)\\s+\" \\ 171 | \"SourceURL:(\\S+)\" 172 | MARKER_BLOCK_RE = re.compile(MARKER_BLOCK) 173 | 174 | DEFAULT_HTML_BODY = \ 175 | \"\" \\ 176 | \"%s\" 177 | 178 | def __init__(self): 179 | self.html = None 180 | self.fragment = None 181 | self.selection = None 182 | self.source = None 183 | self.htmlClipboardVersion = None 184 | 185 | 186 | def GetCfHtml(self): 187 | \"\"\" 188 | Return the FORMATID of the HTML format 189 | \"\"\" 190 | if self.CF_HTML is None: 191 | self.CF_HTML = win32clipboard.RegisterClipboardFormat(\"HTML Format\") 192 | 193 | return self.CF_HTML 194 | 195 | 196 | def GetAvailableFormats(self): 197 | \"\"\" 198 | Return a possibly empty list of formats available on the clipboard 199 | \"\"\" 200 | formats = [] 201 | try: 202 | win32clipboard.OpenClipboard(0) 203 | cf = win32clipboard.EnumClipboardFormats(0) 204 | while (cf != 0): 205 | formats.append(cf) 206 | cf = win32clipboard.EnumClipboardFormats(cf) 207 | finally: 208 | win32clipboard.CloseClipboard() 209 | 210 | return formats 211 | 212 | 213 | def HasHtmlFormat(self): 214 | \"\"\" 215 | Return a boolean indicating if the clipboard has data in HTML format 216 | \"\"\" 217 | return (self.GetCfHtml() in self.GetAvailableFormats()) 218 | 219 | 220 | def GetFromClipboard(self): 221 | \"\"\" 222 | Read and decode the HTML from the clipboard 223 | \"\"\" 224 | 225 | try: 226 | win32clipboard.OpenClipboard(0) 227 | src = win32clipboard.GetClipboardData(self.GetCfHtml()) 228 | self.DecodeClipboardSource(src.decode('utf-8')) 229 | finally: 230 | win32clipboard.CloseClipboard() 231 | 232 | 233 | def DecodeClipboardSource(self, src): 234 | \"\"\" 235 | Decode the given string to figure out the details of the HTML that's on the string 236 | \"\"\" 237 | # Try the extended format first (which has an explicit selection) 238 | matches = self.MARKER_BLOCK_EX_RE.match(src) 239 | if matches: 240 | self.prefix = matches.group(0) 241 | self.htmlClipboardVersion = matches.group(1) 242 | self.html = src[int(matches.group(2)):int(matches.group(3))] 243 | self.fragment = src[int(matches.group(4)):int(matches.group(5))] 244 | self.selection = src[int(matches.group(6)):int(matches.group(7))] 245 | self.source = matches.group(8) 246 | else: 247 | # Failing that, try the version without a selection 248 | matches = self.MARKER_BLOCK_RE.match(src) 249 | if matches: 250 | self.prefix = matches.group(0) 251 | self.htmlClipboardVersion = matches.group(1) 252 | self.html = src[int(matches.group(2)):int(matches.group(3))] 253 | self.fragment = src[int(matches.group(4)):int(matches.group(5))] 254 | self.source = matches.group(6) 255 | self.selection = self.fragment 256 | 257 | 258 | def GetHtml(self, refresh=False): 259 | \"\"\" 260 | Return the entire Html document 261 | \"\"\" 262 | if not self.html or refresh: 263 | self.GetFromClipboard() 264 | return self.html 265 | 266 | 267 | def GetFragment(self, refresh=False): 268 | \"\"\" 269 | Return the Html fragment. A fragment is well-formated HTML enclosing the selected text 270 | \"\"\" 271 | if not self.fragment or refresh: 272 | self.GetFromClipboard() 273 | return self.fragment 274 | 275 | 276 | def GetSelection(self, refresh=False): 277 | \"\"\" 278 | Return the part of the HTML that was selected. It might not be well-formed. 279 | \"\"\" 280 | if not self.selection or refresh: 281 | self.GetFromClipboard() 282 | return self.selection 283 | 284 | 285 | def GetSource(self, refresh=False): 286 | \"\"\" 287 | Return the URL of the source of this HTML 288 | \"\"\" 289 | if not self.selection or refresh: 290 | self.GetFromClipboard() 291 | return self.source 292 | 293 | 294 | def PutFragment(self, fragment, selection=None, html=None, source=None): 295 | \"\"\" 296 | Put the given well-formed fragment of Html into the clipboard. 297 | 298 | selection, if given, must be a literal string within fragment. 299 | html, if given, must be a well-formed Html document that textually 300 | contains fragment and its required markers. 301 | \"\"\" 302 | if selection is None: 303 | selection = fragment 304 | if html is None: 305 | html = self.DEFAULT_HTML_BODY % fragment 306 | if source is None: 307 | source = \"\" 308 | 309 | fragmentStart = html.index(fragment) 310 | fragmentEnd = fragmentStart + len(fragment) 311 | selectionStart = html.index(selection) 312 | selectionEnd = selectionStart + len(selection) 313 | self.PutToClipboard(html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source) 314 | 315 | 316 | def PutToClipboard(self, html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source=\"None\"): 317 | \"\"\" 318 | Replace the Clipboard contents with the given html information. 319 | \"\"\" 320 | 321 | try: 322 | win32clipboard.OpenClipboard(0) 323 | win32clipboard.EmptyClipboard() 324 | src = self.EncodeClipboardSource(html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source) 325 | win32clipboard.SetClipboardData(self.GetCfHtml(), src.encode('utf-8')) 326 | finally: 327 | win32clipboard.CloseClipboard() 328 | 329 | 330 | def EncodeClipboardSource(self, html, fragmentStart, fragmentEnd, selectionStart, selectionEnd, source): 331 | \"\"\" 332 | Join all our bits of information into a string formatted as per the HTML format specs. 333 | \"\"\" 334 | # How long is the prefix going to be? 335 | dummyPrefix = self.MARKER_BLOCK_OUTPUT % (0, 0, 0, 0, 0, 0, source) 336 | lenPrefix = len(dummyPrefix) 337 | 338 | prefix = self.MARKER_BLOCK_OUTPUT % (lenPrefix, len(html)+lenPrefix, 339 | fragmentStart+lenPrefix, fragmentEnd+lenPrefix, 340 | selectionStart+lenPrefix, selectionEnd+lenPrefix, 341 | source) 342 | return (prefix + html) 343 | 344 | 345 | def DumpHtml(): 346 | 347 | cb = HtmlClipboard() 348 | print(\"GetAvailableFormats()=%s\" % str(cb.GetAvailableFormats())) 349 | print(\"HasHtmlFormat()=%s\" % str(cb.HasHtmlFormat())) 350 | if cb.HasHtmlFormat(): 351 | cb.GetFromClipboard() 352 | print(\"prefix=>>>%s<<>>%s<<>>%s<<>>%s<<>>%s<<>>%s<<