ox-publish: Small refactoring
[org-mode.git] / lisp / ox-publish.el
blob87253a5b31212e89b11157067d25f86348e0ad18
1 ;;; ox-publish.el --- Publish Related Org Mode Files as a Website
2 ;; Copyright (C) 2006-2013 Free Software Foundation, Inc.
4 ;; Author: David O'Toole <dto@gnu.org>
5 ;; Maintainer: Carsten Dominik <carsten DOT dominik AT gmail DOT com>
6 ;; Keywords: hypermedia, outlines, wp
8 ;; This file is not part of GNU Emacs.
9 ;;
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23 ;;; Commentary:
25 ;; This program allow configurable publishing of related sets of
26 ;; Org mode files as a complete website.
28 ;; ox-publish.el can do the following:
30 ;; + Publish all one's Org files to a given export back-end
31 ;; + Upload HTML, images, attachments and other files to a web server
32 ;; + Exclude selected private pages from publishing
33 ;; + Publish a clickable sitemap of pages
34 ;; + Manage local timestamps for publishing only changed files
35 ;; + Accept plugin functions to extend range of publishable content
37 ;; Documentation for publishing is in the manual.
39 ;;; Code:
41 (eval-when-compile (require 'cl))
42 (require 'format-spec)
43 (require 'ox)
47 ;;; Variables
49 (defvar org-publish-temp-files nil
50 "Temporary list of files to be published.")
52 ;; Here, so you find the variable right before it's used the first time:
53 (defvar org-publish-cache nil
54 "This will cache timestamps and titles for files in publishing projects.
55 Blocks could hash sha1 values here.")
57 (defgroup org-publish nil
58 "Options for publishing a set of Org-mode and related files."
59 :tag "Org Publishing"
60 :group 'org)
62 (defcustom org-publish-project-alist nil
63 "Association list to control publishing behavior.
64 Each element of the alist is a publishing 'project.' The CAR of
65 each element is a string, uniquely identifying the project. The
66 CDR of each element is in one of the following forms:
68 1. A well-formed property list with an even number of elements,
69 alternating keys and values, specifying parameters for the
70 publishing process.
72 \(:property value :property value ... )
74 2. A meta-project definition, specifying of a list of
75 sub-projects:
77 \(:components \(\"project-1\" \"project-2\" ...))
79 When the CDR of an element of org-publish-project-alist is in
80 this second form, the elements of the list after `:components'
81 are taken to be components of the project, which group together
82 files requiring different publishing options. When you publish
83 such a project with \\[org-publish], the components all
84 publish.
86 When a property is given a value in
87 `org-publish-project-alist', its setting overrides the value of
88 the corresponding user variable \(if any) during publishing.
89 However, options set within a file override everything.
91 Most properties are optional, but some should always be set:
93 `:base-directory'
95 Directory containing publishing source files.
97 `:base-extension'
99 Extension \(without the dot!) of source files. This can be
100 a regular expression. If not given, \"org\" will be used as
101 default extension.
103 `:publishing-directory'
105 Directory \(possibly remote) where output files will be
106 published.
108 The `:exclude' property may be used to prevent certain files from
109 being published. Its value may be a string or regexp matching
110 file names you don't want to be published.
112 The `:include' property may be used to include extra files. Its
113 value may be a list of filenames to include. The filenames are
114 considered relative to the base directory.
116 When both `:include' and `:exclude' properties are given values,
117 the exclusion step happens first.
119 One special property controls which back-end function to use for
120 publishing files in the project. This can be used to extend the
121 set of file types publishable by `org-publish', as well as the
122 set of output formats.
124 `:publishing-function'
126 Function to publish file. Each back-end may define its
127 own (i.e. `org-latex-publish-to-pdf',
128 `org-html-publish-to-html'). May be a list of functions,
129 in which case each function in the list is invoked in turn.
131 Another property allows you to insert code that prepares
132 a project for publishing. For example, you could call GNU Make
133 on a certain makefile, to ensure published files are built up to
134 date.
136 `:preparation-function'
138 Function to be called before publishing this project. This
139 may also be a list of functions.
141 `:completion-function'
143 Function to be called after publishing this project. This
144 may also be a list of functions.
146 Some properties control details of the Org publishing process,
147 and are equivalent to the corresponding user variables listed in
148 the right column. Back-end specific properties may also be
149 included. See the back-end documentation for more information.
151 :author `user-full-name'
152 :creator `org-export-creator-string'
153 :email `user-mail-address'
154 :exclude-tags `org-export-exclude-tags'
155 :headline-levels `org-export-headline-levels'
156 :language `org-export-default-language'
157 :preserve-breaks `org-export-preserve-breaks'
158 :section-numbers `org-export-with-section-numbers'
159 :select-tags `org-export-select-tags'
160 :time-stamp-file `org-export-time-stamp-file'
161 :with-archived-trees `org-export-with-archived-trees'
162 :with-author `org-export-with-author'
163 :with-creator `org-export-with-creator'
164 :with-date `org-export-with-date'
165 :with-drawers `org-export-with-drawers'
166 :with-email `org-export-with-email'
167 :with-emphasize `org-export-with-emphasize'
168 :with-entities `org-export-with-entities'
169 :with-fixed-width `org-export-with-fixed-width'
170 :with-footnotes `org-export-with-footnotes'
171 :with-inlinetasks `org-export-with-inlinetasks'
172 :with-latex `org-export-with-latex'
173 :with-priority `org-export-with-priority'
174 :with-smart-quotes `org-export-with-smart-quotes'
175 :with-special-strings `org-export-with-special-strings'
176 :with-statistics-cookies' `org-export-with-statistics-cookies'
177 :with-sub-superscript `org-export-with-sub-superscripts'
178 :with-toc `org-export-with-toc'
179 :with-tables `org-export-with-tables'
180 :with-tags `org-export-with-tags'
181 :with-tasks `org-export-with-tasks'
182 :with-timestamps `org-export-with-timestamps'
183 :with-todo-keywords `org-export-with-todo-keywords'
185 The following properties may be used to control publishing of
186 a site-map of files or summary page for a given project.
188 `:auto-sitemap'
190 Whether to publish a site-map during
191 `org-publish-current-project' or `org-publish-all'.
193 `:sitemap-filename'
195 Filename for output of sitemap. Defaults to \"sitemap.org\".
197 `:sitemap-title'
199 Title of site-map page. Defaults to name of file.
201 `:sitemap-function'
203 Plugin function to use for generation of site-map. Defaults to
204 `org-publish-org-sitemap', which generates a plain list of
205 links to all files in the project.
207 `:sitemap-style'
209 Can be `list' \(site-map is just an itemized list of the
210 titles of the files involved) or `tree' \(the directory
211 structure of the source files is reflected in the site-map).
212 Defaults to `tree'.
214 `:sitemap-sans-extension'
216 Remove extension from site-map's file-names. Useful to have
217 cool URIs \(see http://www.w3.org/Provider/Style/URI).
218 Defaults to nil.
220 If you create a site-map file, adjust the sorting like this:
222 `:sitemap-sort-folders'
224 Where folders should appear in the site-map. Set this to
225 `first' \(default) or `last' to display folders first or
226 last, respectively. Any other value will mix files and
227 folders.
229 `:sitemap-sort-files'
231 The site map is normally sorted alphabetically. You can
232 change this behaviour setting this to `anti-chronologically',
233 `chronologically', or nil.
235 `:sitemap-ignore-case'
237 Should sorting be case-sensitive? Default nil.
239 The following properties control the creation of a concept index.
241 `:makeindex'
243 Create a concept index.
245 Other properties affecting publication.
247 `:body-only'
249 Set this to t to publish only the body of the documents."
250 :group 'org-export-publish
251 :type 'alist)
253 (defcustom org-publish-use-timestamps-flag t
254 "Non-nil means use timestamp checking to publish only changed files.
255 When nil, do no timestamp checking and always publish all files."
256 :group 'org-export-publish
257 :type 'boolean)
259 (defcustom org-publish-timestamp-directory
260 (convert-standard-filename "~/.org-timestamps/")
261 "Name of directory in which to store publishing timestamps."
262 :group 'org-export-publish
263 :type 'directory)
265 (defcustom org-publish-list-skipped-files t
266 "Non-nil means show message about files *not* published."
267 :group 'org-export-publish
268 :type 'boolean)
270 (defcustom org-publish-sitemap-sort-files 'alphabetically
271 "Method to sort files in site-maps.
272 Possible values are `alphabetically', `chronologically',
273 `anti-chronologically' and nil.
275 If `alphabetically', files will be sorted alphabetically. If
276 `chronologically', files will be sorted with older modification
277 time first. If `anti-chronologically', files will be sorted with
278 newer modification time first. nil won't sort files.
280 You can overwrite this default per project in your
281 `org-publish-project-alist', using `:sitemap-sort-files'."
282 :group 'org-export-publish
283 :type 'symbol)
285 (defcustom org-publish-sitemap-sort-folders 'first
286 "A symbol, denoting if folders are sorted first in sitemaps.
287 Possible values are `first', `last', and nil.
288 If `first', folders will be sorted before files.
289 If `last', folders are sorted to the end after the files.
290 Any other value will not mix files and folders.
292 You can overwrite this default per project in your
293 `org-publish-project-alist', using `:sitemap-sort-folders'."
294 :group 'org-export-publish
295 :type 'symbol)
297 (defcustom org-publish-sitemap-sort-ignore-case nil
298 "Non-nil when site-map sorting should ignore case.
300 You can overwrite this default per project in your
301 `org-publish-project-alist', using `:sitemap-ignore-case'."
302 :group 'org-export-publish
303 :type 'boolean)
305 (defcustom org-publish-sitemap-date-format "%Y-%m-%d"
306 "Format for `format-time-string' which is used to print a date
307 in the sitemap."
308 :group 'org-export-publish
309 :type 'string)
311 (defcustom org-publish-sitemap-file-entry-format "%t"
312 "Format string for site-map file entry.
313 You could use brackets to delimit on what part the link will be.
315 %t is the title.
316 %a is the author.
317 %d is the date formatted using `org-publish-sitemap-date-format'."
318 :group 'org-export-publish
319 :type 'string)
323 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
324 ;;; Timestamp-related functions
326 (defun org-publish-timestamp-filename (filename &optional pub-dir pub-func)
327 "Return path to timestamp file for filename FILENAME."
328 (setq filename (concat filename "::" (or pub-dir "") "::"
329 (format "%s" (or pub-func ""))))
330 (concat "X" (if (fboundp 'sha1) (sha1 filename) (md5 filename))))
332 (defun org-publish-needed-p
333 (filename &optional pub-dir pub-func true-pub-dir base-dir)
334 "Non-nil if FILENAME should be published in PUB-DIR using PUB-FUNC.
335 TRUE-PUB-DIR is where the file will truly end up. Currently we
336 are not using this - maybe it can eventually be used to check if
337 the file is present at the target location, and how old it is.
338 Right now we cannot do this, because we do not know under what
339 file name the file will be stored - the publishing function can
340 still decide about that independently."
341 (let ((rtn (if (not org-publish-use-timestamps-flag) t
342 (org-publish-cache-file-needs-publishing
343 filename pub-dir pub-func base-dir))))
344 (if rtn (message "Publishing file %s using `%s'" filename pub-func)
345 (when org-publish-list-skipped-files
346 (message "Skipping unmodified file %s" filename)))
347 rtn))
349 (defun org-publish-update-timestamp
350 (filename &optional pub-dir pub-func base-dir)
351 "Update publishing timestamp for file FILENAME.
352 If there is no timestamp, create one."
353 (let ((key (org-publish-timestamp-filename filename pub-dir pub-func))
354 (stamp (org-publish-cache-ctime-of-src filename)))
355 (org-publish-cache-set key stamp)))
357 (defun org-publish-remove-all-timestamps ()
358 "Remove all files in the timestamp directory."
359 (let ((dir org-publish-timestamp-directory)
360 files)
361 (when (and (file-exists-p dir) (file-directory-p dir))
362 (mapc 'delete-file (directory-files dir 'full "[^.]\\'"))
363 (org-publish-reset-cache))))
367 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
368 ;;; Getting project information out of `org-publish-project-alist'
370 (defun org-publish-expand-projects (projects-alist)
371 "Expand projects in PROJECTS-ALIST.
372 This splices all the components into the list."
373 (let ((rest projects-alist) rtn p components)
374 (while (setq p (pop rest))
375 (if (setq components (plist-get (cdr p) :components))
376 (setq rest (append
377 (mapcar (lambda (x) (assoc x org-publish-project-alist))
378 components)
379 rest))
380 (push p rtn)))
381 (nreverse (delete-dups (delq nil rtn)))))
383 (defvar org-sitemap-sort-files)
384 (defvar org-sitemap-sort-folders)
385 (defvar org-sitemap-ignore-case)
386 (defvar org-sitemap-requested)
387 (defvar org-sitemap-date-format)
388 (defvar org-sitemap-file-entry-format)
389 (defun org-publish-compare-directory-files (a b)
390 "Predicate for `sort', that sorts folders and files for sitemap."
391 (let ((retval t))
392 (when (or org-sitemap-sort-files org-sitemap-sort-folders)
393 ;; First we sort files:
394 (when org-sitemap-sort-files
395 (case org-sitemap-sort-files
396 (alphabetically
397 (let* ((adir (file-directory-p a))
398 (aorg (and (string-match "\\.org$" a) (not adir)))
399 (bdir (file-directory-p b))
400 (borg (and (string-match "\\.org$" b) (not bdir)))
401 (A (if aorg (concat (file-name-directory a)
402 (org-publish-find-title a)) a))
403 (B (if borg (concat (file-name-directory b)
404 (org-publish-find-title b)) b)))
405 (setq retval (if org-sitemap-ignore-case
406 (not (string-lessp (upcase B) (upcase A)))
407 (not (string-lessp B A))))))
408 ((anti-chronologically chronologically)
409 (let* ((adate (org-publish-find-date a))
410 (bdate (org-publish-find-date b))
411 (A (+ (lsh (car adate) 16) (cadr adate)))
412 (B (+ (lsh (car bdate) 16) (cadr bdate))))
413 (setq retval
414 (if (eq org-sitemap-sort-files 'chronologically) (<= A B)
415 (>= A B)))))))
416 ;; Directory-wise wins:
417 (when org-sitemap-sort-folders
418 ;; a is directory, b not:
419 (cond
420 ((and (file-directory-p a) (not (file-directory-p b)))
421 (setq retval (equal org-sitemap-sort-folders 'first)))
422 ;; a is not a directory, but b is:
423 ((and (not (file-directory-p a)) (file-directory-p b))
424 (setq retval (equal org-sitemap-sort-folders 'last))))))
425 retval))
427 (defun org-publish-get-base-files-1
428 (base-dir &optional recurse match skip-file skip-dir)
429 "Set `org-publish-temp-files' with files from BASE-DIR directory.
430 If RECURSE is non-nil, check BASE-DIR recursively. If MATCH is
431 non-nil, restrict this list to the files matching the regexp
432 MATCH. If SKIP-FILE is non-nil, skip file matching the regexp
433 SKIP-FILE. If SKIP-DIR is non-nil, don't check directories
434 matching the regexp SKIP-DIR when recursing through BASE-DIR."
435 (mapc (lambda (f)
436 (let ((fd-p (file-directory-p f))
437 (fnd (file-name-nondirectory f)))
438 (if (and fd-p recurse
439 (not (string-match "^\\.+$" fnd))
440 (if skip-dir (not (string-match skip-dir fnd)) t))
441 (org-publish-get-base-files-1
442 f recurse match skip-file skip-dir)
443 (unless (or fd-p ;; this is a directory
444 (and skip-file (string-match skip-file fnd))
445 (not (file-exists-p (file-truename f)))
446 (not (string-match match fnd)))
448 (pushnew f org-publish-temp-files)))))
449 (if org-sitemap-requested
450 (sort (directory-files base-dir t (unless recurse match))
451 'org-publish-compare-directory-files)
452 (directory-files base-dir t (unless recurse match)))))
454 (defun org-publish-get-base-files (project &optional exclude-regexp)
455 "Return a list of all files in PROJECT.
456 If EXCLUDE-REGEXP is set, this will be used to filter out
457 matching filenames."
458 (let* ((project-plist (cdr project))
459 (base-dir (file-name-as-directory
460 (plist-get project-plist :base-directory)))
461 (include-list (plist-get project-plist :include))
462 (recurse (plist-get project-plist :recursive))
463 (extension (or (plist-get project-plist :base-extension) "org"))
464 ;; sitemap-... variables are dynamically scoped for
465 ;; org-publish-compare-directory-files:
466 (org-sitemap-requested
467 (plist-get project-plist :auto-sitemap))
468 (sitemap-filename
469 (or (plist-get project-plist :sitemap-filename) "sitemap.org"))
470 (org-sitemap-sort-folders
471 (if (plist-member project-plist :sitemap-sort-folders)
472 (plist-get project-plist :sitemap-sort-folders)
473 org-publish-sitemap-sort-folders))
474 (org-sitemap-sort-files
475 (cond ((plist-member project-plist :sitemap-sort-files)
476 (plist-get project-plist :sitemap-sort-files))
477 ;; For backward compatibility:
478 ((plist-member project-plist :sitemap-alphabetically)
479 (if (plist-get project-plist :sitemap-alphabetically)
480 'alphabetically nil))
481 (t org-publish-sitemap-sort-files)))
482 (org-sitemap-ignore-case
483 (if (plist-member project-plist :sitemap-ignore-case)
484 (plist-get project-plist :sitemap-ignore-case)
485 org-publish-sitemap-sort-ignore-case))
486 (match (if (eq extension 'any) "^[^\\.]"
487 (concat "^[^\\.].*\\.\\(" extension "\\)$"))))
488 ;; Make sure `org-sitemap-sort-folders' has an accepted value
489 (unless (memq org-sitemap-sort-folders '(first last))
490 (setq org-sitemap-sort-folders nil))
492 (setq org-publish-temp-files nil)
493 (if org-sitemap-requested
494 (pushnew (expand-file-name (concat base-dir sitemap-filename))
495 org-publish-temp-files))
496 (org-publish-get-base-files-1 base-dir recurse match
497 ;; FIXME distinguish exclude regexp
498 ;; for skip-file and skip-dir?
499 exclude-regexp exclude-regexp)
500 (mapc (lambda (f)
501 (pushnew
502 (expand-file-name (concat base-dir f))
503 org-publish-temp-files))
504 include-list)
505 org-publish-temp-files))
507 (defun org-publish-get-project-from-filename (filename &optional up)
508 "Return the project that FILENAME belongs to."
509 (let* ((filename (expand-file-name filename))
510 project-name)
512 (catch 'p-found
513 (dolist (prj org-publish-project-alist)
514 (unless (plist-get (cdr prj) :components)
515 ;; [[info:org:Selecting%20files]] shows how this is supposed to work:
516 (let* ((r (plist-get (cdr prj) :recursive))
517 (b (expand-file-name (file-name-as-directory
518 (plist-get (cdr prj) :base-directory))))
519 (x (or (plist-get (cdr prj) :base-extension) "org"))
520 (e (plist-get (cdr prj) :exclude))
521 (i (plist-get (cdr prj) :include))
522 (xm (concat "^" b (if r ".+" "[^/]+") "\\.\\(" x "\\)$")))
523 (when
524 (or (and i
525 (member filename
526 (mapcar (lambda (file)
527 (expand-file-name file b))
528 i)))
529 (and (not (and e (string-match e filename)))
530 (string-match xm filename)))
531 (setq project-name (car prj))
532 (throw 'p-found project-name))))))
533 (when up
534 (dolist (prj org-publish-project-alist)
535 (if (member project-name (plist-get (cdr prj) :components))
536 (setq project-name (car prj)))))
537 (assoc project-name org-publish-project-alist)))
541 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
542 ;;; Tools for publishing functions in back-ends
544 (defun org-publish-org-to (backend filename extension plist &optional pub-dir)
545 "Publish an Org file to a specified back-end.
547 BACKEND is a symbol representing the back-end used for
548 transcoding. FILENAME is the filename of the Org file to be
549 published. EXTENSION is the extension used for the output
550 string, with the leading dot. PLIST is the property list for the
551 given project.
553 Optional argument PUB-DIR, when non-nil is the publishing
554 directory.
556 Return output file name."
557 (unless (or (not pub-dir) (file-exists-p pub-dir)) (make-directory pub-dir t))
558 ;; Check if a buffer visiting FILENAME is already open.
559 (let* ((visitingp (find-buffer-visiting filename))
560 (work-buffer (or visitingp (find-file-noselect filename))))
561 (prog1 (with-current-buffer work-buffer
562 (let ((output-file
563 (org-export-output-file-name extension nil pub-dir))
564 (body-p (plist-get plist :body-only)))
565 (org-export-to-file
566 backend output-file nil nil body-p
567 ;; Install `org-publish-collect-index' in parse tree
568 ;; filters. It isn't dependent on `:makeindex', since
569 ;; we want to keep it up-to-date in cache anyway.
570 (org-combine-plists
571 plist '(:filter-parse-tree (org-publish-collect-index))))))
572 ;; Remove opened buffer in the process.
573 (unless visitingp (kill-buffer work-buffer)))))
575 (defvar project-plist)
577 (defun org-publish-attachment (plist filename pub-dir)
578 "Publish a file with no transformation of any kind.
580 FILENAME is the filename of the Org file to be published. PLIST
581 is the property list for the given project. PUB-DIR is the
582 publishing directory.
584 Return output file name."
585 (unless (file-directory-p pub-dir)
586 (make-directory pub-dir t))
587 (or (equal (expand-file-name (file-name-directory filename))
588 (file-name-as-directory (expand-file-name pub-dir)))
589 (copy-file filename
590 (expand-file-name (file-name-nondirectory filename) pub-dir)
591 t)))
595 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
596 ;;; Publishing files, sets of files, and indices
598 (defun org-publish-file (filename &optional project no-cache)
599 "Publish file FILENAME from PROJECT.
600 If NO-CACHE is not nil, do not initialize org-publish-cache and
601 write it to disk. This is needed, since this function is used to
602 publish single files, when entire projects are published.
603 See `org-publish-projects'."
604 (let* ((project
605 (or project
606 (or (org-publish-get-project-from-filename filename)
607 (error "File %s not part of any known project"
608 (abbreviate-file-name filename)))))
609 (project-plist (cdr project))
610 (ftname (expand-file-name filename))
611 (publishing-function
612 (or (plist-get project-plist :publishing-function)
613 (error "No publishing function chosen")))
614 (base-dir
615 (file-name-as-directory
616 (expand-file-name
617 (or (plist-get project-plist :base-directory)
618 (error "Project %s does not have :base-directory defined"
619 (car project))))))
620 (pub-dir
621 (file-name-as-directory
622 (file-truename
623 (or (eval (plist-get project-plist :publishing-directory))
624 (error "Project %s does not have :publishing-directory defined"
625 (car project))))))
626 tmp-pub-dir)
628 (unless no-cache (org-publish-initialize-cache (car project)))
630 (setq tmp-pub-dir
631 (file-name-directory
632 (concat pub-dir
633 (and (string-match (regexp-quote base-dir) ftname)
634 (substring ftname (match-end 0))))))
635 (if (listp publishing-function)
636 ;; allow chain of publishing functions
637 (mapc (lambda (f)
638 (when (org-publish-needed-p
639 filename pub-dir f tmp-pub-dir base-dir)
640 (funcall f project-plist filename tmp-pub-dir)
641 (org-publish-update-timestamp filename pub-dir f base-dir)))
642 publishing-function)
643 (when (org-publish-needed-p
644 filename pub-dir publishing-function tmp-pub-dir base-dir)
645 (funcall publishing-function project-plist filename tmp-pub-dir)
646 (org-publish-update-timestamp
647 filename pub-dir publishing-function base-dir)))
648 (unless no-cache (org-publish-write-cache-file))))
650 (defun org-publish-projects (projects)
651 "Publish all files belonging to the PROJECTS alist.
652 If `:auto-sitemap' is set, publish the sitemap too. If
653 `:makeindex' is set, also produce a file theindex.org."
654 (mapc
655 (lambda (project)
656 ;; Each project uses its own cache file:
657 (org-publish-initialize-cache (car project))
658 (let* ((project-plist (cdr project))
659 (exclude-regexp (plist-get project-plist :exclude))
660 (sitemap-p (plist-get project-plist :auto-sitemap))
661 (sitemap-filename (or (plist-get project-plist :sitemap-filename)
662 "sitemap.org"))
663 (sitemap-function (or (plist-get project-plist :sitemap-function)
664 'org-publish-org-sitemap))
665 (org-sitemap-date-format
666 (or (plist-get project-plist :sitemap-date-format)
667 org-publish-sitemap-date-format))
668 (org-sitemap-file-entry-format
669 (or (plist-get project-plist :sitemap-file-entry-format)
670 org-publish-sitemap-file-entry-format))
671 (preparation-function
672 (plist-get project-plist :preparation-function))
673 (completion-function (plist-get project-plist :completion-function))
674 (files (org-publish-get-base-files project exclude-regexp)) file)
675 (when preparation-function (run-hooks 'preparation-function))
676 (if sitemap-p (funcall sitemap-function project sitemap-filename))
677 (dolist (file files) (org-publish-file file project t))
678 (when (plist-get project-plist :makeindex)
679 (org-publish-index-generate-theindex
680 project (plist-get project-plist :base-directory))
681 (org-publish-file
682 (expand-file-name
683 "theindex.org" (plist-get project-plist :base-directory))
684 project t))
685 (when completion-function (run-hooks 'completion-function))
686 (org-publish-write-cache-file)))
687 (org-publish-expand-projects projects)))
689 (defun org-publish-org-sitemap (project &optional sitemap-filename)
690 "Create a sitemap of pages in set defined by PROJECT.
691 Optionally set the filename of the sitemap with SITEMAP-FILENAME.
692 Default for SITEMAP-FILENAME is 'sitemap.org'."
693 (let* ((project-plist (cdr project))
694 (dir (file-name-as-directory
695 (plist-get project-plist :base-directory)))
696 (localdir (file-name-directory dir))
697 (indent-str (make-string 2 ?\ ))
698 (exclude-regexp (plist-get project-plist :exclude))
699 (files (nreverse
700 (org-publish-get-base-files project exclude-regexp)))
701 (sitemap-filename (concat dir (or sitemap-filename "sitemap.org")))
702 (sitemap-title (or (plist-get project-plist :sitemap-title)
703 (concat "Sitemap for project " (car project))))
704 (sitemap-style (or (plist-get project-plist :sitemap-style)
705 'tree))
706 (sitemap-sans-extension
707 (plist-get project-plist :sitemap-sans-extension))
708 (visiting (find-buffer-visiting sitemap-filename))
709 (ifn (file-name-nondirectory sitemap-filename))
710 file sitemap-buffer)
711 (with-current-buffer (setq sitemap-buffer
712 (or visiting (find-file sitemap-filename)))
713 (erase-buffer)
714 (insert (concat "#+TITLE: " sitemap-title "\n\n"))
715 (while (setq file (pop files))
716 (let ((fn (file-name-nondirectory file))
717 (link (file-relative-name file dir))
718 (oldlocal localdir))
719 (when sitemap-sans-extension
720 (setq link (file-name-sans-extension link)))
721 ;; sitemap shouldn't list itself
722 (unless (equal (file-truename sitemap-filename)
723 (file-truename file))
724 (if (eq sitemap-style 'list)
725 (message "Generating list-style sitemap for %s" sitemap-title)
726 (message "Generating tree-style sitemap for %s" sitemap-title)
727 (setq localdir (concat (file-name-as-directory dir)
728 (file-name-directory link)))
729 (unless (string= localdir oldlocal)
730 (if (string= localdir dir)
731 (setq indent-str (make-string 2 ?\ ))
732 (let ((subdirs
733 (split-string
734 (directory-file-name
735 (file-name-directory
736 (file-relative-name localdir dir))) "/"))
737 (subdir "")
738 (old-subdirs (split-string
739 (file-relative-name oldlocal dir) "/")))
740 (setq indent-str (make-string 2 ?\ ))
741 (while (string= (car old-subdirs) (car subdirs))
742 (setq indent-str (concat indent-str (make-string 2 ?\ )))
743 (pop old-subdirs)
744 (pop subdirs))
745 (dolist (d subdirs)
746 (setq subdir (concat subdir d "/"))
747 (insert (concat indent-str " + " d "\n"))
748 (setq indent-str (make-string
749 (+ (length indent-str) 2) ?\ )))))))
750 ;; This is common to 'flat and 'tree
751 (let ((entry
752 (org-publish-format-file-entry
753 org-sitemap-file-entry-format file project-plist))
754 (regexp "\\(.*\\)\\[\\([^][]+\\)\\]\\(.*\\)"))
755 (cond ((string-match-p regexp entry)
756 (string-match regexp entry)
757 (insert (concat indent-str " + " (match-string 1 entry)
758 "[[file:" link "]["
759 (match-string 2 entry)
760 "]]" (match-string 3 entry) "\n")))
762 (insert (concat indent-str " + [[file:" link "]["
763 entry
764 "]]\n"))))))))
765 (save-buffer))
766 (or visiting (kill-buffer sitemap-buffer))))
768 (defun org-publish-format-file-entry (fmt file project-plist)
769 (format-spec fmt
770 `((?t . ,(org-publish-find-title file t))
771 (?d . ,(format-time-string org-sitemap-date-format
772 (org-publish-find-date file)))
773 (?a . ,(or (plist-get project-plist :author) user-full-name)))))
775 (defun org-publish-find-title (file &optional reset)
776 "Find the title of FILE in project."
778 (and (not reset) (org-publish-cache-get-file-property file :title nil t))
779 (let* ((visiting (find-buffer-visiting file))
780 (buffer (or visiting (find-file-noselect file)))
781 title)
782 (with-current-buffer buffer
783 (org-mode)
784 (setq title
785 (or (org-element-interpret-data
786 (plist-get (org-export-get-environment) :title))
787 (file-name-nondirectory (file-name-sans-extension file)))))
788 (unless visiting (kill-buffer buffer))
789 (org-publish-cache-set-file-property file :title title)
790 title)))
792 (defun org-publish-find-date (file)
793 "Find the date of FILE in project.
794 If FILE provides a #+date keyword use it else use the file
795 system's modification time.
797 It returns time in `current-time' format."
798 (let* ((visiting (find-buffer-visiting file))
799 (file-buf (or visiting (find-file-noselect file nil)))
800 (date (plist-get
801 (with-current-buffer file-buf
802 (org-mode)
803 (org-export--get-inbuffer-options))
804 :date)))
805 (unless visiting (kill-buffer file-buf))
806 (if date (org-time-string-to-time date)
807 (when (file-exists-p file)
808 (nth 5 (file-attributes file))))))
812 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
813 ;;; Interactive publishing functions
815 ;;;###autoload
816 (defalias 'org-publish-project 'org-publish)
818 ;;;###autoload
819 (defun org-publish (project &optional force async)
820 "Publish PROJECT.
822 PROJECT is either a project name, as a string, or a project
823 alist (see `org-publish-project-alist' variable).
825 When optional argument FORCE is non-nil, force publishing all
826 files in PROJECT. With a non-nil optional argument ASYNC,
827 publishing will be done asynchronously, in another process."
828 (interactive
829 (list
830 (assoc (org-icompleting-read
831 "Publish project: "
832 org-publish-project-alist nil t)
833 org-publish-project-alist)
834 current-prefix-arg))
835 (let ((project-alist (if (not (stringp project)) (list project)
836 ;; If this function is called in batch mode,
837 ;; project is still a string here.
838 (list (assoc project org-publish-project-alist)))))
839 (if async
840 (org-export-async-start 'ignore
841 `(let ((org-publish-use-timestamps-flag
842 (if ',force nil ,org-publish-use-timestamps-flag)))
843 (org-publish-projects ',project-alist)))
844 (save-window-excursion
845 (let* ((org-publish-use-timestamps-flag
846 (if force nil org-publish-use-timestamps-flag)))
847 (org-publish-projects project-alist))))))
849 ;;;###autoload
850 (defun org-publish-all (&optional force async)
851 "Publish all projects.
852 With prefix argument FORCE, remove all files in the timestamp
853 directory and force publishing all projects. With a non-nil
854 optional argument ASYNC, publishing will be done asynchronously,
855 in another process."
856 (interactive "P")
857 (if async
858 (org-export-async-start 'ignore
859 `(when ',force (org-publish-remove-all-timestamps))
860 `(let ((org-publish-use-timestamps-flag
861 (if ',force nil ,org-publish-use-timestamps-flag)))
862 (org-publish-projects ',org-publish-project-alist)))
863 (when force (org-publish-remove-all-timestamps))
864 (save-window-excursion
865 (let ((org-publish-use-timestamps-flag
866 (if force nil org-publish-use-timestamps-flag)))
867 (org-publish-projects org-publish-project-alist)))))
870 ;;;###autoload
871 (defun org-publish-current-file (&optional force async)
872 "Publish the current file.
873 With prefix argument FORCE, force publish the file. When
874 optional argument ASYNC is non-nil, publishing will be done
875 asynchronously, in another process."
876 (interactive "P")
877 (let ((file (buffer-file-name (buffer-base-buffer))))
878 (if async
879 (org-export-async-start 'ignore
880 `(let ((org-publish-use-timestamps-flag
881 (if ',force nil ,org-publish-use-timestamps-flag)))
882 (org-publish-file ,file)))
883 (save-window-excursion
884 (let ((org-publish-use-timestamps-flag
885 (if force nil org-publish-use-timestamps-flag)))
886 (org-publish-file file))))))
888 ;;;###autoload
889 (defun org-publish-current-project (&optional force async)
890 "Publish the project associated with the current file.
891 With a prefix argument, force publishing of all files in
892 the project."
893 (interactive "P")
894 (save-window-excursion
895 (let ((project (org-publish-get-project-from-filename
896 (buffer-file-name (buffer-base-buffer)) 'up)))
897 (if project (org-publish project force async)
898 (error "File %s is not part of any known project"
899 (buffer-file-name (buffer-base-buffer)))))))
903 ;;; Index generation
905 (defun org-publish-collect-index (tree backend info)
906 "Update index for a file with TREE in cache.
908 BACKEND is the back-end being used for transcoding. INFO is
909 a plist containing publishing options.
911 The index relative to current file is stored as an alist. An
912 association has the following shape: (TERM FILE-NAME PARENT),
913 where TERM is the indexed term, as a string, FILE-NAME is the
914 original full path of the file where the term in encountered, and
915 PARENT is a reference to the headline, if any, containing the
916 original index keyword. When non-nil, this reference is a cons
917 cell. Its CAR is a symbol among `id', `custom-id' and `name' and
918 its CDR is a string."
919 (let ((file (plist-get info :input-file)))
920 (org-publish-cache-set-file-property
921 file :index
922 (delete-dups
923 (org-element-map tree 'keyword
924 (lambda (k)
925 (when (equal (upcase (org-element-property :key k)) "INDEX")
926 (let ((parent (org-export-get-parent-headline k)))
927 (list (org-element-property :value k)
928 file
929 (cond
930 ((not parent) nil)
931 ((let ((id (org-element-property :id parent)))
932 (and id (cons 'id id))))
933 ((let ((id (org-element-property :custom-id parent)))
934 (and id (cons 'custom-id id))))
935 (t (cons 'name
936 (org-element-property :raw-value parent))))))))
937 info))))
938 ;; Return parse-tree to avoid altering output.
939 tree)
941 (defun org-publish-index-generate-theindex (project directory)
942 "Retrieve full index from cache and build \"theindex.org\".
943 PROJECT is the project the index relates to. DIRECTORY is the
944 publishing directory."
945 (let ((all-files (org-publish-get-base-files
946 project (plist-get (cdr project) :exclude)))
947 full-index)
948 ;; Compile full index.
949 (mapc
950 (lambda (file)
951 (let ((index (org-publish-cache-get-file-property file :index)))
952 (dolist (term index)
953 (unless (member term full-index) (push term full-index)))))
954 all-files)
955 ;; Sort it alphabetically.
956 (setq full-index
957 (sort full-index (lambda (a b) (string< (downcase (car a))
958 (downcase (car b))))))
959 ;; Fill "theindex.org".
960 (with-temp-buffer
961 (insert "#+TITLE: Index\n#+OPTIONS: num:nil author:nil\n")
962 (let ((current-letter nil) (last-entry nil))
963 (dolist (idx full-index)
964 (let* ((entry (org-split-string (car idx) "!"))
965 (letter (upcase (substring (car entry) 0 1)))
966 ;; Transform file into a path relative to publishing
967 ;; directory.
968 (file (file-relative-name
969 (nth 1 idx)
970 (plist-get (cdr project) :base-directory))))
971 ;; Check if another letter has to be inserted.
972 (unless (string= letter current-letter)
973 (insert (format "* %s\n" letter)))
974 ;; Compute the first difference between last entry and
975 ;; current one: it tells the level at which new items
976 ;; should be added.
977 (let* ((rank (loop for n from 0 to (length entry)
978 unless (equal (nth n entry) (nth n last-entry))
979 return n))
980 (len (length (nthcdr rank entry))))
981 ;; For each term after the first difference, create
982 ;; a new sub-list with the term as body. Moreover,
983 ;; linkify the last term.
984 (dotimes (n len)
985 (insert
986 (concat
987 (make-string (* (+ rank n) 2) ? ) " - "
988 (if (not (= (1- len) n)) (nth (+ rank n) entry)
989 ;; Last term: Link it to TARGET, if possible.
990 (let ((target (nth 2 idx)))
991 (format
992 "[[%s][%s]]"
993 ;; Destination.
994 (case (car target)
995 ('nil (format "file:%s" file))
996 (id (format "id:%s" (cdr target)))
997 (custom-id (format "file:%s::#%s" file (cdr target)))
998 (otherwise (format "file:%s::*%s" file (cdr target))))
999 ;; Description.
1000 (car (last entry)))))
1001 "\n"))))
1002 (setq current-letter letter last-entry entry))))
1003 ;; Write index.
1004 (write-file (expand-file-name "theindex.org" directory)))))
1008 ;;; Caching functions
1010 (defun org-publish-write-cache-file (&optional free-cache)
1011 "Write `org-publish-cache' to file.
1012 If FREE-CACHE, empty the cache."
1013 (unless org-publish-cache
1014 (error "`org-publish-write-cache-file' called, but no cache present"))
1016 (let ((cache-file (org-publish-cache-get ":cache-file:")))
1017 (unless cache-file
1018 (error "Cannot find cache-file name in `org-publish-write-cache-file'"))
1019 (with-temp-file cache-file
1020 (let (print-level print-length)
1021 (insert "(setq org-publish-cache (make-hash-table :test 'equal :weakness nil :size 100))\n")
1022 (maphash (lambda (k v)
1023 (insert
1024 (format (concat "(puthash %S "
1025 (if (or (listp v) (symbolp v))
1026 "'" "")
1027 "%S org-publish-cache)\n") k v)))
1028 org-publish-cache)))
1029 (when free-cache (org-publish-reset-cache))))
1031 (defun org-publish-initialize-cache (project-name)
1032 "Initialize the projects cache if not initialized yet and return it."
1034 (unless project-name
1035 (error "Cannot initialize `org-publish-cache' without projects name in `org-publish-initialize-cache'"))
1037 (unless (file-exists-p org-publish-timestamp-directory)
1038 (make-directory org-publish-timestamp-directory t))
1039 (unless (file-directory-p org-publish-timestamp-directory)
1040 (error "Org publish timestamp: %s is not a directory"
1041 org-publish-timestamp-directory))
1043 (unless (and org-publish-cache
1044 (string= (org-publish-cache-get ":project:") project-name))
1045 (let* ((cache-file
1046 (concat
1047 (expand-file-name org-publish-timestamp-directory)
1048 project-name ".cache"))
1049 (cexists (file-exists-p cache-file)))
1051 (when org-publish-cache (org-publish-reset-cache))
1053 (if cexists (load-file cache-file)
1054 (setq org-publish-cache
1055 (make-hash-table :test 'equal :weakness nil :size 100))
1056 (org-publish-cache-set ":project:" project-name)
1057 (org-publish-cache-set ":cache-file:" cache-file))
1058 (unless cexists (org-publish-write-cache-file nil))))
1059 org-publish-cache)
1061 (defun org-publish-reset-cache ()
1062 "Empty org-publish-cache and reset it nil."
1063 (message "%s" "Resetting org-publish-cache")
1064 (when (hash-table-p org-publish-cache)
1065 (clrhash org-publish-cache))
1066 (setq org-publish-cache nil))
1068 (defun org-publish-cache-file-needs-publishing
1069 (filename &optional pub-dir pub-func base-dir)
1070 "Check the timestamp of the last publishing of FILENAME.
1071 Non-nil if the file needs publishing. The function also checks
1072 if any included files have been more recently published, so that
1073 the file including them will be republished as well."
1074 (unless org-publish-cache
1075 (error
1076 "`org-publish-cache-file-needs-publishing' called, but no cache present"))
1077 (let* ((case-fold-search t)
1078 (key (org-publish-timestamp-filename filename pub-dir pub-func))
1079 (pstamp (org-publish-cache-get key))
1080 (visiting (find-buffer-visiting filename))
1081 included-files-ctime buf)
1083 (when (equal (file-name-extension filename) "org")
1084 (setq buf (find-file (expand-file-name filename)))
1085 (with-current-buffer buf
1086 (goto-char (point-min))
1087 (while (re-search-forward
1088 "^#\\+INCLUDE:[ \t]+\"\\([^\t\n\r\"]*\\)\"[ \t]*.*$" nil t)
1089 (let* ((included-file (expand-file-name (match-string 1))))
1090 (add-to-list 'included-files-ctime
1091 (org-publish-cache-ctime-of-src included-file) t))))
1092 (unless visiting (kill-buffer buf)))
1093 (if (null pstamp) t
1094 (let ((ctime (org-publish-cache-ctime-of-src filename)))
1095 (or (< pstamp ctime)
1096 (when included-files-ctime
1097 (not (null (delq nil (mapcar (lambda(ct) (< ctime ct))
1098 included-files-ctime))))))))))
1100 (defun org-publish-cache-set-file-property
1101 (filename property value &optional project-name)
1102 "Set the VALUE for a PROPERTY of file FILENAME in publishing cache to VALUE.
1103 Use cache file of PROJECT-NAME. If the entry does not exist, it
1104 will be created. Return VALUE."
1105 ;; Evtl. load the requested cache file:
1106 (if project-name (org-publish-initialize-cache project-name))
1107 (let ((pl (org-publish-cache-get filename)))
1108 (if pl (progn (plist-put pl property value) value)
1109 (org-publish-cache-get-file-property
1110 filename property value nil project-name))))
1112 (defun org-publish-cache-get-file-property
1113 (filename property &optional default no-create project-name)
1114 "Return the value for a PROPERTY of file FILENAME in publishing cache.
1115 Use cache file of PROJECT-NAME. Return the value of that PROPERTY
1116 or DEFAULT, if the value does not yet exist. If the entry will
1117 be created, unless NO-CREATE is not nil."
1118 ;; Evtl. load the requested cache file:
1119 (if project-name (org-publish-initialize-cache project-name))
1120 (let ((pl (org-publish-cache-get filename)) retval)
1121 (if pl
1122 (if (plist-member pl property)
1123 (setq retval (plist-get pl property))
1124 (setq retval default))
1125 ;; no pl yet:
1126 (unless no-create
1127 (org-publish-cache-set filename (list property default)))
1128 (setq retval default))
1129 retval))
1131 (defun org-publish-cache-get (key)
1132 "Return the value stored in `org-publish-cache' for key KEY.
1133 Returns nil, if no value or nil is found, or the cache does not
1134 exist."
1135 (unless org-publish-cache
1136 (error "`org-publish-cache-get' called, but no cache present"))
1137 (gethash key org-publish-cache))
1139 (defun org-publish-cache-set (key value)
1140 "Store KEY VALUE pair in `org-publish-cache'.
1141 Returns value on success, else nil."
1142 (unless org-publish-cache
1143 (error "`org-publish-cache-set' called, but no cache present"))
1144 (puthash key value org-publish-cache))
1146 (defun org-publish-cache-ctime-of-src (file)
1147 "Get the ctime of FILE as an integer."
1148 (let ((attr (file-attributes
1149 (expand-file-name (or (file-symlink-p file) file)
1150 (file-name-directory file)))))
1151 (+ (lsh (car (nth 5 attr)) 16)
1152 (cadr (nth 5 attr)))))
1155 (provide 'ox-publish)
1157 ;; Local variables:
1158 ;; generated-autoload-file: "org-loaddefs.el"
1159 ;; End:
1161 ;;; ox-publish.el ends here