1 ;;; org-fireforg.el --- provide functionality for interaction of
2 ;;; Fireforg, a Firefox extension, with org mode
5 ;; Copyright 2009 Andreas Burtzlaff
7 ;; Author: Andreas Burtzlaff < andreas at burtzlaff dot de >
9 ;; Keywords: org-mode firefox annotations
11 ;; This file is not part of GNU Emacs.
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.
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.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program; if not, write to the Free Software
25 ;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 ;; fireforg-show-annotation://<file (encoded)>/<header (encoded)>
33 ;; Opens the given file in emacs and searches for header
35 ;; fireforg-bibtex-entry://<BibTeX entry (encoded)>
37 ;; Sends a BibTeX entry that is formatted according to `org-fireforg-received-bibtex-format'
38 ;; and put into the kill ring
42 (require 'org-protocol
)
46 (add-to-list 'org-protocol-protocol-alist
47 '("Fireforg show annotation: fireforg-show-annotation://<file (encoded)>/<header (encoded)>"
48 :protocol
"fireforg-show-annotation"
49 :function org-fireforg-show-annotation
))
51 (add-to-list 'org-protocol-protocol-alist
52 '("Fireforg get bibtex entry: fireforg-bibtex-entry://<bibtex entry (encoded)>"
53 :protocol
"fireforg-bibtex-entry"
54 :function org-fireforg-receive-bibtex-entry
))
56 (defgroup org-fireforg nil
57 "Options for the Fireforg extension of Org-mode."
60 (defcustom org-fireforg-received-bibtex-format
'heading
61 "Non-nil means, transform bibtex entries.
63 If the variable is `headers' the entry is transformed into a
64 heading with the bibtex entries as properties prefixed with
65 `BIB_'. The CUSTOM_ID is set to the bibtex key."
68 (const :tag
"Create heading with properties" heading
)
69 (const :tag
"BibTex" nil
)
70 (const :tag
"Create heading with properties and BibTeX entry as content" headingWithPropsAndBibTeXContent
)
71 (const :tag
"Create heading with BibTeX entry as content" headingWithBibTeXContent
)))
73 ;; Searches for header in given file
74 (defun org-fireforg-show-annotation (data)
75 (let* ((arguments (org-protocol-split-data data t
))
76 (file (nth 0 arguments
))
77 (heading (nth 1 arguments
))
78 (frameList (or (visible-frame-list) (frame-list) )))
80 (goto-char (point-min))
81 (re-search-forward (regexp-quote heading
))
84 (if frameList
(select-frame-set-input-focus (car frameList
)))
88 ;; Renamed functions of rewritten org-registry.el
89 ;; Temporarily moved here to avoid confusing.
93 ;; This needs to be customizable
94 (defun org-fireforg-registry-file-set () (org-agenda-files))
96 (defcustom org-fireforg-registry-file
97 (concat (getenv "HOME") "/.org-fireforg-registry.el")
98 "The Org registry file."
99 :group
'org-fireforg-registry
102 (defcustom org-fireforg-registry-file-xml
103 (concat (getenv "HOME") "/.org-fireforg-registry.xml")
104 "The Org registry file in xml format. Used by fireforg."
105 :group
'org-fireforg-registry
108 (defcustom org-fireforg-registry-find-file
'find-file-other-window
109 "How to find visit files."
111 :group
'org-fireforg-registry
)
113 (defvar org-fireforg-registry-alist nil
114 "An alist containing the Org registry.")
118 (defun org-fireforg-registry-initialize (&optional from-scratch
)
119 "Initialize `org-fireforg-registry-alist'.
120 If FROM-SCRATCH is non-nil or the registry does not exist yet,
121 create a new registry from scratch and eval it. If the registry
122 exists, eval `org-fireforg-registry-file' and make it the new value for
123 `org-fireforg-registry-alist'."
125 ;;(message (concat "org-fireforg-registry-initialize: org-agenda-files = " (with-output-to-string (prin1 org-agenda-files)))) ;; DEBUG
126 (cond ((or from-scratch
(not (file-exists-p org-fireforg-registry-file
)))
127 ;; create a new registry
128 (setq org-fireforg-registry-alist nil
)
131 (setq org-fireforg-registry-alist
(org-fireforg-registry-get-entries (expand-file-name file
) org-fireforg-registry-alist
)))
132 (org-fireforg-registry-file-set))
134 ;; (when from-scratch
135 (org-fireforg-registry-create org-fireforg-registry-alist
)
136 (org-fireforg-registry-create-xml org-fireforg-registry-alist
))
138 ;; eval the registry file
140 (insert-file-contents org-fireforg-registry-file
)
141 ;; (eval-buffer) ;; reloading the registry is not working yet. Use (org-fireforg-registry-initialize t) for the time being.
143 (org-fireforg-registry-create-xml org-fireforg-registry-alist
))))
146 (defun org-fireforg-registry-insinuate ()
147 "Call `org-fireforg-registry-update' after saving in Org-mode.
148 Use with caution. This could slow down things a bit."
150 (add-hook 'org-mode-hook
151 (lambda() (add-hook 'after-save-hook
152 'org-fireforg-registry-update t t
))))
154 ;; Warning: complex data structure ahead.
155 (defun org-fireforg-registry-get-entries (currentFile &optional registry
)
156 "Merge all Org links in FILE into the registry."
159 (add-entry-for (function (lambda (link desc
)
160 (let* ((point (match-beginning 0))
161 (onHeading (org-on-heading-p))
165 (if (org-before-first-heading-p) (cons "" nil
)
166 (org-back-to-heading t
)
167 (setq headingPoint
(point))
168 (let ((ohc (org-heading-components)))
169 (list (nth 4 ohc
) (org-get-tags-at) (org-entry-get headingPoint
"BIB_doi"))))))
170 (heading (car headingTagsAndDOI
))
171 (tags (nth 1 headingTagsAndDOI
))
172 (doi (if (nth 2 headingTagsAndDOI
) (org-fireforg-bibtex-trim-string (nth 2 headingTagsAndDOI
))))
173 (contentEntry (list point desc onHeading
))
174 (headingEntry (list heading headingPoint onHeading tags
(list contentEntry
) doi
))
175 (fileEntry (list (expand-file-name currentFile
) (list headingEntry
)))
176 (linkEntry (list link
(list fileEntry
)))
177 (existingLinkEntry (assoc link result
))
178 (existingFileEntry (assoc (expand-file-name currentFile
) (nth 1 existingLinkEntry
)))
179 (existingHeadingEntry (assoc heading
(nth 1 existingFileEntry
))))
181 (cond (existingLinkEntry
182 (cond (existingFileEntry
183 (cond (existingHeadingEntry (setf (nth 4 existingHeadingEntry
) (cons contentEntry
(nth 4 existingHeadingEntry
))))
184 (t (setf (nth 1 existingFileEntry
) (cons headingEntry
(nth 1 existingFileEntry
))))))
185 (t (setf (nth 1 existingLinkEntry
) (cons fileEntry
(nth 1 existingLinkEntry
))))))
186 (t (add-to-list 'result linkEntry
))))))
191 (insert-file-contents currentFile
)
192 ;; Turn on org-mode in order to use org-heading-components and org-get-tags-at
193 ;; This can have severe impact on performance for large files, so I want to get rid of this requirement.
195 (goto-char (point-min))
196 (while (re-search-forward org-bracket-link-regexp nil t
)
197 (funcall add-entry-for
(match-string-no-properties 1) (or (match-string-no-properties 3) "No description")))
198 (goto-char (point-min))
199 (while (re-search-forward org-angle-link-re nil t
)
200 (funcall add-entry-for
(concat (match-string-no-properties 1) ":" (match-string-no-properties 2)) "" ))
201 (goto-char (point-min))
202 (while (re-search-forward org-plain-link-re nil t
)
203 (funcall add-entry-for
(match-string-no-properties 0) "" ))
204 (goto-char (point-min))
205 ;; add all DOI's in properties as urls with prefix "http://dx.doi.org/"
208 (let ((doi (org-entry-get (point) "BIB_doi")))
209 ;;(message (concat "current file:" currentFile))
211 (funcall add-entry-for
213 ;; Due to a bug in Zotero it might happen that
214 ;; the doi identifier is enclosed in two sets
216 ;; Therefore the function org-fireforg-bibtex-trim-string is apply two times.
217 (org-fireforg-doi-to-url (org-fireforg-bibtex-trim-string (org-fireforg-bibtex-trim-string doi
)))
219 (nth 4 (org-heading-components)))))))
223 (defun org-fireforg-registry-update ()
224 "Update the registry for the current Org file, if it is in org-fireforg-registry-file-set."
226 (unless (org-mode-p) (error "Not in org-mode"))
227 (cond ((not (file-exists-p org-fireforg-registry-file
))
228 ;; registry-file doesn't exist -> create it from scratch
229 (org-fireforg-registry-initialize t
))
231 ;; update existing registry-file
232 (let ((from-file (expand-file-name (buffer-file-name))))
233 (cond ((member from-file
(mapcar 'expand-file-name
(org-fireforg-registry-file-set)))
234 (let ((registryFiltered (org-fireforg-registry-filter-where-filename-not from-file org-fireforg-registry-alist
)))
235 (setq org-fireforg-registry-alist
(org-fireforg-registry-get-entries from-file registryFiltered
))
236 (org-fireforg-registry-create org-fireforg-registry-alist
)
237 (org-fireforg-registry-create-xml org-fireforg-registry-alist
)
238 ;;(message (format "Org registry updated for %s. Found %i entries. Registry contains %i entries." (file-name-nondirectory from-file) (length new-entries) (length org-fireforg-registry-alist)))
240 ;; (t (message (format "Current file %s is not in org-fireforg-registry-file-set." from-file)))
243 ;; requires expanded filename as argument
244 (defun org-fireforg-registry-filter-where-filename-not (filename registry
)
245 (cond ((not registry
) nil
)
246 ((nlistp registry
) (error "org-fireforg-registry-filter-where-filename-not: argument registry is not a list"))
248 (mapcar (lambda (linkEntry)
249 (list (car linkEntry
)
250 (org-fireforg-registry-filter
252 (not (string= (expand-file-name (car fileEntry
)) filename
)))
257 (defun org-fireforg-registry-create (entries)
258 "Create `org-fireforg-registry-file' with ENTRIES."
261 (find-file org-fireforg-registry-file
)
264 (concat ";; -*- emacs-lisp -*-\n"
266 ";; You shouldn't try to modify this buffer manually\n\n"
267 "(setq org-fireforg-registry-alist\n"
268 (org-fireforg-registry-to-string-rec org-fireforg-registry-alist
) ")"))
270 (set-buffer-file-coding-system 'utf-8
)
272 (kill-buffer (current-buffer))))
273 (message "Org registry created"))
275 (defun org-fireforg-registry-to-string-rec (obj)
276 "Return elisp code that generates an object with identical content as the argument"
277 (cond ((not obj
) "nil")
278 ((stringp obj
) (concat "\"" obj
"\""))
279 ((nlistp obj
) (with-output-to-string (prin1 obj
)))
281 (concat "(list " (mapconcat 'org-fireforg-registry-to-string-rec obj
" ") ")")
286 (defun org-fireforg-registry-create-xml (entries)
287 "Create org-fireforg-registry-file-xml with ENTRIES in xml format."
290 (concat "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<orgregistry>\n"
291 (reduce 'concat
(mapcar (lambda (linkEntry)
292 (concat "<link url=\"" (url-insert-entities-in-string (nth 0 linkEntry
)) "\">\n"
293 (reduce 'concat
(mapcar (lambda (fileEntry)
294 (let ((file (nth 0 fileEntry
)))
295 (reduce 'concat
(mapcar (lambda (headingEntry)
296 (concat " <heading file=\"" (url-insert-entities-in-string file
) "\" "
297 "text=\"" (url-insert-entities-in-string (nth 0 headingEntry
)) "\" "
298 "point=\"" (number-to-string (nth 1 headingEntry
)) "\" "
299 "linkInHeading=\"" (url-insert-entities-in-string (if (nth 2 headingEntry
) "t" "f")) "\" "
300 "tags=\"" (if (nth 3 headingEntry
)
301 (url-insert-entities-in-string (concat ":" (mapconcat 'identity
(nth 3 headingEntry
) ":") ":"))
303 (if (nth 5 headingEntry
) (concat "doi=\"" (nth 5 headingEntry
) "\" ") "")
305 (reduce 'concat
(mapcar (lambda (contentEntry)
306 (concat " <contentEntry point=\"" (number-to-string (nth 0 contentEntry
)) "\" "
307 "description=\"" (url-insert-entities-in-string (nth 1 contentEntry
)) "\" "
308 "inHeading=\"" (if (nth 2 contentEntry
) "t" "f") "\"/>\n"))
309 (nth 4 headingEntry
)) :initial-value
"")
311 (nth 1 fileEntry
)) :initial-value
"")
313 ) (nth 1 linkEntry
)) :initial-value
"")
314 "</link>\n")) entries
) :initial-value
"")
317 (when (file-writable-p org-fireforg-registry-file-xml
)
318 (set-buffer-file-coding-system 'utf-8
)
319 (write-region (point-min)
321 org-fireforg-registry-file-xml
))))
323 (defun org-fireforg-registry-filter (condp lst
)
325 (mapcar (lambda (x) (and (funcall condp x
) x
)) lst
)))
327 (defun org-fireforg-receive-bibtex-entry (data)
328 ;;(message "Received bibtex string") ;; DEBUG
329 (let* ((arguments (org-protocol-split-data data t
))
330 (bibtex (nth 0 arguments
))
331 (bibtexParsed (org-fireforg-parse-bibtex-entry-wrapper bibtex
)))
332 (kill-new (cond ((eq org-fireforg-received-bibtex-format
'heading
)
333 (concat (org-fireforg-generate-heading bibtexParsed
) "\n" (org-fireforg-bibtex-entry-to-properties bibtexParsed
)))
334 ((eq org-fireforg-received-bibtex-format
'headingWithPropsAndBibTeXContent
)
335 (concat (org-fireforg-generate-heading bibtexParsed
) "\n" (org-fireforg-bibtex-entry-to-properties bibtexParsed
) bibtex
"\n"))
336 ((eq org-fireforg-received-bibtex-format
'headingWithBibTeXContent
)
337 (concat (org-fireforg-generate-heading bibtexParsed
) "\n" bibtex
"\n"))
338 ((not org-fireforg-received-bibtex-format
) bibtex
)))
339 (message "Saved BibTeX entry to kill ring.")
343 (defun org-fireforg-parse-bibtex-entry-wrapper (bibtexEntryString)
345 (insert bibtexEntryString
)
346 (goto-char (point-min))
347 ;;(bibtex-next-field t)
348 (re-search-forward "@")
349 (goto-char (match-beginning 0))
350 (bibtex-parse-entry)))
352 (defun org-fireforg-bibtex-trim-string (string)
353 (replace-regexp-in-string "[\"}] *$" "" (replace-regexp-in-string "^ *[{\"]" "" string
)))
355 (defun org-fireforg-headings-to-bibtex (&optional match
)
356 (reduce 'concat
(org-map-entries (lambda () (concat (org-fireforg-heading-to-bibtex-entry) "\n\n")) match
)))
358 (defun org-fireforg-heading-to-bibtex-entry ()
359 (let* ((properties (org-entry-properties))
360 (type (cdr (assoc "BIB_entryType" properties
)))
361 (id (cdr (assoc "CUSTOM_ID" properties
)))
362 (properties (rassq-delete-all id
(rassq-delete-all type properties
))))
364 (concat "@" type
"{" id
366 (mapcar (lambda (prop) (if (and (> (length (car prop
)) 4) (string= (substring (car prop
) 0 4) "BIB_")) (concat ",\n " (substring (car prop
) 4) " = " (cdr prop
) ))) properties
) :initial-value
"") "\n}")))))
368 (defun org-fireforg-bibtex-entry-to-properties (bibtexEntry)
369 (concat ":PROPERTIES:\n"
370 (reduce 'concat
(mapcar (lambda (entry)
371 (concat (cond ((string= (car entry
) "=key=") " :CUSTOM_ID")
372 ((string= (car entry
) "=type=") " :BIB_entryType")
373 (t (concat " :BIB_" (car entry
) ))) ": " (cdr entry
) "\n")) bibtexEntry
) :initial-value
"")
376 (defun org-fireforg-generate-heading (bibtexEntry)
377 (let ((heading (concat "* [[" (org-fireforg-bibtex-trim-string (cdr (assoc "url" bibtexEntry
))) "][" (org-fireforg-bibtex-trim-string (cdr (assoc "title" bibtexEntry
))) "]]" )))
378 ;;(with-temp-buffer (insert heading) (goto-char (point-min)) (org-id-get-create) (buffer-substring (point-min) (point-max)))
382 ;; exports the bibtex properties of the current buffer to selectable file
383 (defun org-fireforg-export-bibtex-to-file (file)
386 (cond ((not file
) (error "No file supplied"))
387 ((let ((bibtex (org-fireforg-headings-to-bibtex)))
390 (write-file file t
)))))))
392 (defun org-fireforg-export-bibtex-to-new-buffer ()
394 (let ((bibtex (org-fireforg-headings-to-bibtex))
395 ;; find nearest bibliography entry before point
396 (prevId (save-excursion
397 (while (and (not (org-before-first-heading-p)) (if (org-at-heading-p) (or (null (org-entry-get (point) "CUSTOM_ID")) (string= (org-entry-get (point) "CUSTOM_ID") "")) 1) (not (bobp)) ) (progn (backward-char) (org-back-to-heading t
) ) )
398 (if (or (org-before-first-heading-p) (bobp)) nil
(org-entry-get (point) "CUSTOM_ID")))))
399 (switch-to-buffer (generate-new-buffer "*BibTeX export*"))
401 (goto-char (point-min))
402 (if prevId
(progn (re-search-forward (regexp-quote prevId
)) (beginning-of-line)))
405 (defun org-fireforg-doi-to-url (string)
406 (concat "http://dx.doi.org/"
407 (replace-regexp-in-string " " "%20"
408 (replace-regexp-in-string "#" "%23"
409 (replace-regexp-in-string "\"" "%22"
410 (replace-regexp-in-string "%" "%25" string
))))))
412 (provide 'org-fireforg
)