commit | author | age
|
76bbd0
|
1 |
;;; ox-publish.el --- Publish Related Org Mode Files as a Website -*- lexical-binding: t; -*- |
C |
2 |
;; Copyright (C) 2006-2018 Free Software Foundation, Inc. |
|
3 |
|
|
4 |
;; Author: David O'Toole <dto@gnu.org> |
|
5 |
;; Maintainer: Carsten Dominik <carsten DOT dominik AT gmail DOT com> |
|
6 |
;; Keywords: hypermedia, outlines, wp |
|
7 |
|
|
8 |
;; This file is 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. |
|
14 |
|
|
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. |
|
19 |
|
|
20 |
;; You should have received a copy of the GNU General Public License |
|
21 |
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. |
|
22 |
|
|
23 |
;;; Commentary: |
|
24 |
|
|
25 |
;; This program allow configurable publishing of related sets of |
|
26 |
;; Org mode files as a complete website. |
|
27 |
;; |
|
28 |
;; ox-publish.el can do the following: |
|
29 |
;; |
|
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 |
|
36 |
;; |
|
37 |
;; Documentation for publishing is in the manual. |
|
38 |
|
|
39 |
;;; Code: |
|
40 |
|
|
41 |
(require 'cl-lib) |
|
42 |
(require 'format-spec) |
|
43 |
(require 'ox) |
|
44 |
|
|
45 |
|
|
46 |
|
|
47 |
;;; Variables |
|
48 |
|
|
49 |
;; Here, so you find the variable right before it's used the first time: |
|
50 |
(defvar org-publish-cache nil |
|
51 |
"This will cache timestamps and titles for files in publishing projects. |
|
52 |
Blocks could hash sha1 values here.") |
|
53 |
|
|
54 |
(defvar org-publish-after-publishing-hook nil |
|
55 |
"Hook run each time a file is published. |
|
56 |
Every function in this hook will be called with two arguments: |
|
57 |
the name of the original file and the name of the file |
|
58 |
produced.") |
|
59 |
|
|
60 |
(defgroup org-export-publish nil |
|
61 |
"Options for publishing a set of files." |
|
62 |
:tag "Org Publishing" |
|
63 |
:group 'org-export) |
|
64 |
|
|
65 |
(defcustom org-publish-project-alist nil |
|
66 |
"Association list to control publishing behavior. |
|
67 |
\\<org-mode-map> |
|
68 |
Each element of the alist is a publishing project. The car of |
|
69 |
each element is a string, uniquely identifying the project. The |
|
70 |
cdr of each element is in one of the following forms: |
|
71 |
|
|
72 |
1. A well-formed property list with an even number of elements, |
|
73 |
alternating keys and values, specifying parameters for the |
|
74 |
publishing process. |
|
75 |
|
|
76 |
(:property value :property value ... ) |
|
77 |
|
|
78 |
2. A meta-project definition, specifying of a list of |
|
79 |
sub-projects: |
|
80 |
|
|
81 |
(:components (\"project-1\" \"project-2\" ...)) |
|
82 |
|
|
83 |
When the CDR of an element of org-publish-project-alist is in |
|
84 |
this second form, the elements of the list after `:components' |
|
85 |
are taken to be components of the project, which group together |
|
86 |
files requiring different publishing options. When you publish |
|
87 |
such a project with `\\[org-publish]', the components all publish. |
|
88 |
|
|
89 |
When a property is given a value in `org-publish-project-alist', |
|
90 |
its setting overrides the value of the corresponding user |
|
91 |
variable (if any) during publishing. However, options set within |
|
92 |
a file override everything. |
|
93 |
|
|
94 |
Most properties are optional, but some should always be set: |
|
95 |
|
|
96 |
`:base-directory' |
|
97 |
|
|
98 |
Directory containing publishing source files. |
|
99 |
|
|
100 |
`:base-extension' |
|
101 |
|
|
102 |
Extension (without the dot!) of source files. This can be |
|
103 |
a regular expression. If not given, \"org\" will be used as |
|
104 |
default extension. If it is `any', include all the files, |
|
105 |
even without extension. |
|
106 |
|
|
107 |
`:publishing-directory' |
|
108 |
|
|
109 |
Directory (possibly remote) where output files will be |
|
110 |
published. |
|
111 |
|
|
112 |
If `:recursive' is non-nil files in sub-directories of |
|
113 |
`:base-directory' are considered. |
|
114 |
|
|
115 |
The `:exclude' property may be used to prevent certain files from |
|
116 |
being published. Its value may be a string or regexp matching |
|
117 |
file names you don't want to be published. |
|
118 |
|
|
119 |
The `:include' property may be used to include extra files. Its |
|
120 |
value may be a list of filenames to include. The filenames are |
|
121 |
considered relative to the base directory. |
|
122 |
|
|
123 |
When both `:include' and `:exclude' properties are given values, |
|
124 |
the exclusion step happens first. |
|
125 |
|
|
126 |
One special property controls which back-end function to use for |
|
127 |
publishing files in the project. This can be used to extend the |
|
128 |
set of file types publishable by `org-publish', as well as the |
|
129 |
set of output formats. |
|
130 |
|
|
131 |
`:publishing-function' |
|
132 |
|
|
133 |
Function to publish file. Each back-end may define its |
|
134 |
own (i.e. `org-latex-publish-to-pdf', |
|
135 |
`org-html-publish-to-html'). May be a list of functions, in |
|
136 |
which case each function in the list is invoked in turn. |
|
137 |
|
|
138 |
Another property allows you to insert code that prepares |
|
139 |
a project for publishing. For example, you could call GNU Make |
|
140 |
on a certain makefile, to ensure published files are built up to |
|
141 |
date. |
|
142 |
|
|
143 |
`:preparation-function' |
|
144 |
|
|
145 |
Function to be called before publishing this project. This |
|
146 |
may also be a list of functions. Preparation functions are |
|
147 |
called with the project properties list as their sole |
|
148 |
argument. |
|
149 |
|
|
150 |
`:completion-function' |
|
151 |
|
|
152 |
Function to be called after publishing this project. This |
|
153 |
may also be a list of functions. Completion functions are |
|
154 |
called with the project properties list as their sole |
|
155 |
argument. |
|
156 |
|
|
157 |
Some properties control details of the Org publishing process, |
|
158 |
and are equivalent to the corresponding user variables listed in |
|
159 |
the right column. Back-end specific properties may also be |
|
160 |
included. See the back-end documentation for more information. |
|
161 |
|
|
162 |
:author `user-full-name' |
|
163 |
:creator `org-export-creator-string' |
|
164 |
:email `user-mail-address' |
|
165 |
:exclude-tags `org-export-exclude-tags' |
|
166 |
:headline-levels `org-export-headline-levels' |
|
167 |
:language `org-export-default-language' |
|
168 |
:preserve-breaks `org-export-preserve-breaks' |
|
169 |
:section-numbers `org-export-with-section-numbers' |
|
170 |
:select-tags `org-export-select-tags' |
|
171 |
:time-stamp-file `org-export-time-stamp-file' |
|
172 |
:with-archived-trees `org-export-with-archived-trees' |
|
173 |
:with-author `org-export-with-author' |
|
174 |
:with-creator `org-export-with-creator' |
|
175 |
:with-date `org-export-with-date' |
|
176 |
:with-drawers `org-export-with-drawers' |
|
177 |
:with-email `org-export-with-email' |
|
178 |
:with-emphasize `org-export-with-emphasize' |
|
179 |
:with-entities `org-export-with-entities' |
|
180 |
:with-fixed-width `org-export-with-fixed-width' |
|
181 |
:with-footnotes `org-export-with-footnotes' |
|
182 |
:with-inlinetasks `org-export-with-inlinetasks' |
|
183 |
:with-latex `org-export-with-latex' |
|
184 |
:with-planning `org-export-with-planning' |
|
185 |
:with-priority `org-export-with-priority' |
|
186 |
:with-properties `org-export-with-properties' |
|
187 |
:with-smart-quotes `org-export-with-smart-quotes' |
|
188 |
:with-special-strings `org-export-with-special-strings' |
|
189 |
:with-statistics-cookies' `org-export-with-statistics-cookies' |
|
190 |
:with-sub-superscript `org-export-with-sub-superscripts' |
|
191 |
:with-toc `org-export-with-toc' |
|
192 |
:with-tables `org-export-with-tables' |
|
193 |
:with-tags `org-export-with-tags' |
|
194 |
:with-tasks `org-export-with-tasks' |
|
195 |
:with-timestamps `org-export-with-timestamps' |
|
196 |
:with-title `org-export-with-title' |
|
197 |
:with-todo-keywords `org-export-with-todo-keywords' |
|
198 |
|
|
199 |
The following properties may be used to control publishing of |
|
200 |
a site-map of files or summary page for a given project. |
|
201 |
|
|
202 |
`:auto-sitemap' |
|
203 |
|
|
204 |
Whether to publish a site-map during |
|
205 |
`org-publish-current-project' or `org-publish-all'. |
|
206 |
|
|
207 |
`:sitemap-filename' |
|
208 |
|
|
209 |
Filename for output of site-map. Defaults to \"sitemap.org\". |
|
210 |
|
|
211 |
`:sitemap-title' |
|
212 |
|
|
213 |
Title of site-map page. Defaults to name of file. |
|
214 |
|
|
215 |
`:sitemap-style' |
|
216 |
|
|
217 |
Can be `list' (site-map is just an itemized list of the |
|
218 |
titles of the files involved) or `tree' (the directory |
|
219 |
structure of the source files is reflected in the site-map). |
|
220 |
Defaults to `tree'. |
|
221 |
|
|
222 |
`:sitemap-format-entry' |
|
223 |
|
|
224 |
Plugin function used to format entries in the site-map. It |
|
225 |
is called with three arguments: the file or directory name |
|
226 |
relative to base directory, the site map style and the |
|
227 |
current project. It has to return a string. |
|
228 |
|
|
229 |
Defaults to `org-publish-sitemap-default-entry', which turns |
|
230 |
file names into links and use document titles as |
|
231 |
descriptions. For specific formatting needs, one can use |
|
232 |
`org-publish-find-date', `org-publish-find-title' and |
|
233 |
`org-publish-find-property', to retrieve additional |
|
234 |
information about published documents. |
|
235 |
|
|
236 |
`:sitemap-function' |
|
237 |
|
|
238 |
Plugin function to use for generation of site-map. It is |
|
239 |
called with two arguments: the title of the site-map, as |
|
240 |
a string, and a representation of the files involved in the |
|
241 |
project, as returned by `org-list-to-lisp'. The latter can |
|
242 |
further be transformed using `org-list-to-generic', |
|
243 |
`org-list-to-subtree' and alike. It has to return a string. |
|
244 |
|
|
245 |
Defaults to `org-publish-sitemap-default', which generates |
|
246 |
a plain list of links to all files in the project. |
|
247 |
|
|
248 |
If you create a site-map file, adjust the sorting like this: |
|
249 |
|
|
250 |
`:sitemap-sort-folders' |
|
251 |
|
|
252 |
Where folders should appear in the site-map. Set this to |
|
253 |
`first' or `last' to display folders first or last, |
|
254 |
respectively. When set to `ignore' (default), folders are |
|
255 |
ignored altogether. Any other value will mix files and |
|
256 |
folders. This variable has no effect when site-map style is |
|
257 |
`tree'. |
|
258 |
|
|
259 |
`:sitemap-sort-files' |
|
260 |
|
|
261 |
The site map is normally sorted alphabetically. You can |
|
262 |
change this behavior setting this to `anti-chronologically', |
|
263 |
`chronologically', or nil. |
|
264 |
|
|
265 |
`:sitemap-ignore-case' |
|
266 |
|
|
267 |
Should sorting be case-sensitive? Default nil. |
|
268 |
|
|
269 |
The following property control the creation of a concept index. |
|
270 |
|
|
271 |
`:makeindex' |
|
272 |
|
|
273 |
Create a concept index. The file containing the index has to |
|
274 |
be called \"theindex.org\". If it doesn't exist in the |
|
275 |
project, it will be generated. Contents of the index are |
|
276 |
stored in the file \"theindex.inc\", which can be included in |
|
277 |
\"theindex.org\". |
|
278 |
|
|
279 |
Other properties affecting publication. |
|
280 |
|
|
281 |
`:body-only' |
|
282 |
|
|
283 |
Set this to t to publish only the body of the documents." |
|
284 |
:group 'org-export-publish |
|
285 |
:type 'alist) |
|
286 |
|
|
287 |
(defcustom org-publish-use-timestamps-flag t |
|
288 |
"Non-nil means use timestamp checking to publish only changed files. |
|
289 |
When nil, do no timestamp checking and always publish all files." |
|
290 |
:group 'org-export-publish |
|
291 |
:type 'boolean) |
|
292 |
|
|
293 |
(defcustom org-publish-timestamp-directory |
|
294 |
(convert-standard-filename "~/.org-timestamps/") |
|
295 |
"Name of directory in which to store publishing timestamps." |
|
296 |
:group 'org-export-publish |
|
297 |
:type 'directory) |
|
298 |
|
|
299 |
(defcustom org-publish-list-skipped-files t |
|
300 |
"Non-nil means show message about files *not* published." |
|
301 |
:group 'org-export-publish |
|
302 |
:type 'boolean) |
|
303 |
|
|
304 |
(defcustom org-publish-sitemap-sort-files 'alphabetically |
|
305 |
"Method to sort files in site-maps. |
|
306 |
Possible values are `alphabetically', `chronologically', |
|
307 |
`anti-chronologically' and nil. |
|
308 |
|
|
309 |
If `alphabetically', files will be sorted alphabetically. If |
|
310 |
`chronologically', files will be sorted with older modification |
|
311 |
time first. If `anti-chronologically', files will be sorted with |
|
312 |
newer modification time first. nil won't sort files. |
|
313 |
|
|
314 |
You can overwrite this default per project in your |
|
315 |
`org-publish-project-alist', using `:sitemap-sort-files'." |
|
316 |
:group 'org-export-publish |
|
317 |
:type 'symbol) |
|
318 |
|
|
319 |
(defcustom org-publish-sitemap-sort-folders 'ignore |
|
320 |
"A symbol, denoting if folders are sorted first in site-maps. |
|
321 |
|
|
322 |
Possible values are `first', `last', `ignore' and nil. |
|
323 |
If `first', folders will be sorted before files. |
|
324 |
If `last', folders are sorted to the end after the files. |
|
325 |
If `ignore', folders do not appear in the site-map. |
|
326 |
Any other value will mix files and folders. |
|
327 |
|
|
328 |
You can overwrite this default per project in your |
|
329 |
`org-publish-project-alist', using `:sitemap-sort-folders'. |
|
330 |
|
|
331 |
This variable is ignored when site-map style is `tree'." |
|
332 |
:group 'org-export-publish |
|
333 |
:type '(choice |
|
334 |
(const :tag "Folders before files" first) |
|
335 |
(const :tag "Folders after files" last) |
|
336 |
(const :tag "No folder in site-map" ignore) |
|
337 |
(const :tag "Mix folders and files" nil)) |
|
338 |
:version "26.1" |
|
339 |
:package-version '(Org . "9.1") |
|
340 |
:safe #'symbolp) |
|
341 |
|
|
342 |
(defcustom org-publish-sitemap-sort-ignore-case nil |
|
343 |
"Non-nil when site-map sorting should ignore case. |
|
344 |
|
|
345 |
You can overwrite this default per project in your |
|
346 |
`org-publish-project-alist', using `:sitemap-ignore-case'." |
|
347 |
:group 'org-export-publish |
|
348 |
:type 'boolean) |
|
349 |
|
|
350 |
|
|
351 |
|
|
352 |
;;; Timestamp-related functions |
|
353 |
|
|
354 |
(defun org-publish-timestamp-filename (filename &optional pub-dir pub-func) |
|
355 |
"Return path to timestamp file for filename FILENAME." |
|
356 |
(setq filename (concat filename "::" (or pub-dir "") "::" |
|
357 |
(format "%s" (or pub-func "")))) |
|
358 |
(concat "X" (if (fboundp 'sha1) (sha1 filename) (md5 filename)))) |
|
359 |
|
|
360 |
(defun org-publish-needed-p |
|
361 |
(filename &optional pub-dir pub-func _true-pub-dir base-dir) |
|
362 |
"Non-nil if FILENAME should be published in PUB-DIR using PUB-FUNC. |
|
363 |
TRUE-PUB-DIR is where the file will truly end up. Currently we |
|
364 |
are not using this - maybe it can eventually be used to check if |
|
365 |
the file is present at the target location, and how old it is. |
|
366 |
Right now we cannot do this, because we do not know under what |
|
367 |
file name the file will be stored - the publishing function can |
|
368 |
still decide about that independently." |
|
369 |
(let ((rtn (if (not org-publish-use-timestamps-flag) t |
|
370 |
(org-publish-cache-file-needs-publishing |
|
371 |
filename pub-dir pub-func base-dir)))) |
|
372 |
(if rtn (message "Publishing file %s using `%s'" filename pub-func) |
|
373 |
(when org-publish-list-skipped-files |
|
374 |
(message "Skipping unmodified file %s" filename))) |
|
375 |
rtn)) |
|
376 |
|
|
377 |
(defun org-publish-update-timestamp |
|
378 |
(filename &optional pub-dir pub-func _base-dir) |
|
379 |
"Update publishing timestamp for file FILENAME. |
|
380 |
If there is no timestamp, create one." |
|
381 |
(let ((key (org-publish-timestamp-filename filename pub-dir pub-func)) |
|
382 |
(stamp (org-publish-cache-ctime-of-src filename))) |
|
383 |
(org-publish-cache-set key stamp))) |
|
384 |
|
|
385 |
(defun org-publish-remove-all-timestamps () |
|
386 |
"Remove all files in the timestamp directory." |
|
387 |
(let ((dir org-publish-timestamp-directory)) |
|
388 |
(when (and (file-exists-p dir) (file-directory-p dir)) |
|
389 |
(mapc #'delete-file (directory-files dir 'full "[^.]\\'")) |
|
390 |
(org-publish-reset-cache)))) |
|
391 |
|
|
392 |
|
|
393 |
|
|
394 |
;;; Getting project information out of `org-publish-project-alist' |
|
395 |
|
|
396 |
(defun org-publish-property (property project &optional default) |
|
397 |
"Return value PROPERTY, as a symbol, in PROJECT. |
|
398 |
DEFAULT is returned when PROPERTY is not actually set in PROJECT |
|
399 |
definition." |
|
400 |
(let ((properties (cdr project))) |
|
401 |
(if (plist-member properties property) |
|
402 |
(plist-get properties property) |
|
403 |
default))) |
|
404 |
|
|
405 |
(defun org-publish--expand-file-name (file project) |
|
406 |
"Return full file name for FILE in PROJECT. |
|
407 |
When FILE is a relative file name, it is expanded according to |
|
408 |
project base directory." |
|
409 |
(if (file-name-absolute-p file) file |
|
410 |
(expand-file-name file (org-publish-property :base-directory project)))) |
|
411 |
|
|
412 |
(defun org-publish-expand-projects (projects-alist) |
|
413 |
"Expand projects in PROJECTS-ALIST. |
|
414 |
This splices all the components into the list." |
|
415 |
(let ((rest projects-alist) rtn p components) |
|
416 |
(while (setq p (pop rest)) |
|
417 |
(if (setq components (plist-get (cdr p) :components)) |
|
418 |
(setq rest (append |
|
419 |
(mapcar |
|
420 |
(lambda (x) |
|
421 |
(or (assoc x org-publish-project-alist) |
|
422 |
(user-error "Unknown component %S in project %S" |
|
423 |
x (car p)))) |
|
424 |
components) |
|
425 |
rest)) |
|
426 |
(push p rtn))) |
|
427 |
(nreverse (delete-dups (delq nil rtn))))) |
|
428 |
|
|
429 |
(defun org-publish-get-base-files (project) |
|
430 |
"Return a list of all files in PROJECT." |
|
431 |
(let* ((base-dir (file-name-as-directory |
|
432 |
(org-publish-property :base-directory project))) |
|
433 |
(extension (or (org-publish-property :base-extension project) "org")) |
|
434 |
(match (if (eq extension 'any) "" |
|
435 |
(format "^[^\\.].*\\.\\(%s\\)$" extension))) |
|
436 |
(base-files |
|
437 |
(cond ((not (file-exists-p base-dir)) nil) |
|
438 |
((not (org-publish-property :recursive project)) |
|
439 |
(cl-remove-if #'file-directory-p |
|
440 |
(directory-files base-dir t match t))) |
|
441 |
(t |
|
442 |
;; Find all files recursively. Unlike to |
|
443 |
;; `directory-files-recursively', we follow symlinks |
|
444 |
;; to other directories. |
|
445 |
(letrec ((files nil) |
|
446 |
(walk-tree |
|
447 |
(lambda (dir depth) |
|
448 |
(when (> depth 100) |
|
449 |
(error "Apparent cycle of symbolic links for %S" |
|
450 |
base-dir)) |
|
451 |
(dolist (f (file-name-all-completions "" dir)) |
|
452 |
(pcase f |
|
453 |
((or "./" "../") nil) |
|
454 |
((pred directory-name-p) |
|
455 |
(funcall walk-tree |
|
456 |
(expand-file-name f dir) |
|
457 |
(1+ depth))) |
|
458 |
((pred (string-match match)) |
|
459 |
(push (expand-file-name f dir) files)) |
|
460 |
(_ nil))) |
|
461 |
files))) |
|
462 |
(funcall walk-tree base-dir 0)))))) |
|
463 |
(org-uniquify |
|
464 |
(append |
|
465 |
;; Files from BASE-DIR. Apply exclusion filter before adding |
|
466 |
;; included files. |
|
467 |
(let ((exclude-regexp (org-publish-property :exclude project))) |
|
468 |
(if exclude-regexp |
|
469 |
(cl-remove-if |
|
470 |
(lambda (f) |
|
471 |
;; Match against relative names, yet BASE-DIR file |
|
472 |
;; names are absolute. |
|
473 |
(string-match exclude-regexp |
|
474 |
(file-relative-name f base-dir))) |
|
475 |
base-files) |
|
476 |
base-files)) |
|
477 |
;; Sitemap file. |
|
478 |
(and (org-publish-property :auto-sitemap project) |
|
479 |
(list (expand-file-name |
|
480 |
(or (org-publish-property :sitemap-filename project) |
|
481 |
"sitemap.org") |
|
482 |
base-dir))) |
|
483 |
;; Included files. |
|
484 |
(mapcar (lambda (f) (expand-file-name f base-dir)) |
|
485 |
(org-publish-property :include project)))))) |
|
486 |
|
|
487 |
(defun org-publish-get-project-from-filename (filename &optional up) |
|
488 |
"Return a project that FILENAME belongs to. |
|
489 |
When UP is non-nil, return a meta-project (i.e., with a :components part) |
|
490 |
publishing FILENAME." |
|
491 |
(let* ((filename (expand-file-name filename)) |
|
492 |
(project |
|
493 |
(cl-some |
|
494 |
(lambda (p) |
|
495 |
;; Ignore meta-projects. |
|
496 |
(unless (org-publish-property :components p) |
|
497 |
(let ((base (expand-file-name |
|
498 |
(org-publish-property :base-directory p)))) |
|
499 |
(cond |
|
500 |
;; Check if FILENAME is explicitly included in one |
|
501 |
;; project. |
|
502 |
((cl-some (lambda (f) (file-equal-p f filename)) |
|
503 |
(mapcar (lambda (f) (expand-file-name f base)) |
|
504 |
(org-publish-property :include p))) |
|
505 |
p) |
|
506 |
;; Exclude file names matching :exclude property. |
|
507 |
((let ((exclude-re (org-publish-property :exclude p))) |
|
508 |
(and exclude-re |
|
509 |
(string-match-p exclude-re |
|
510 |
(file-relative-name filename base)))) |
|
511 |
nil) |
|
512 |
;; Check :extension. Handle special `any' |
|
513 |
;; extension. |
|
514 |
((let ((extension (org-publish-property :base-extension p))) |
|
515 |
(not (or (eq extension 'any) |
|
516 |
(string= (or extension "org") |
|
517 |
(file-name-extension filename))))) |
|
518 |
nil) |
|
519 |
;; Check if FILENAME belong to project's base |
|
520 |
;; directory, or some of its sub-directories |
|
521 |
;; if :recursive in non-nil. |
|
522 |
((member filename (org-publish-get-base-files p)) p) |
|
523 |
(t nil))))) |
|
524 |
org-publish-project-alist))) |
|
525 |
(cond |
|
526 |
((not project) nil) |
|
527 |
((not up) project) |
|
528 |
;; When optional argument UP is non-nil, return the top-most |
|
529 |
;; meta-project effectively publishing FILENAME. |
|
530 |
(t |
|
531 |
(letrec ((find-parent-project |
|
532 |
(lambda (project) |
|
533 |
(or (cl-some |
|
534 |
(lambda (p) |
|
535 |
(and (member (car project) |
|
536 |
(org-publish-property :components p)) |
|
537 |
(funcall find-parent-project p))) |
|
538 |
org-publish-project-alist) |
|
539 |
project)))) |
|
540 |
(funcall find-parent-project project)))))) |
|
541 |
|
|
542 |
|
|
543 |
|
|
544 |
;;; Tools for publishing functions in back-ends |
|
545 |
|
|
546 |
(defun org-publish-org-to (backend filename extension plist &optional pub-dir) |
|
547 |
"Publish an Org file to a specified back-end. |
|
548 |
|
|
549 |
BACKEND is a symbol representing the back-end used for |
|
550 |
transcoding. FILENAME is the filename of the Org file to be |
|
551 |
published. EXTENSION is the extension used for the output |
|
552 |
string, with the leading dot. PLIST is the property list for the |
|
553 |
given project. |
|
554 |
|
|
555 |
Optional argument PUB-DIR, when non-nil is the publishing |
|
556 |
directory. |
|
557 |
|
|
558 |
Return output file name." |
|
559 |
(unless (or (not pub-dir) (file-exists-p pub-dir)) (make-directory pub-dir t)) |
|
560 |
;; Check if a buffer visiting FILENAME is already open. |
|
561 |
(let* ((org-inhibit-startup t) |
|
562 |
(visiting (find-buffer-visiting filename)) |
|
563 |
(work-buffer (or visiting (find-file-noselect filename)))) |
|
564 |
(unwind-protect |
|
565 |
(with-current-buffer work-buffer |
|
566 |
(let ((output (org-export-output-file-name extension nil pub-dir))) |
|
567 |
(org-export-to-file backend output |
|
568 |
nil nil nil (plist-get plist :body-only) |
|
569 |
;; Add `org-publish--store-crossrefs' and |
|
570 |
;; `org-publish-collect-index' to final output filters. |
|
571 |
;; The latter isn't dependent on `:makeindex', since we |
|
572 |
;; want to keep it up-to-date in cache anyway. |
|
573 |
(org-combine-plists |
|
574 |
plist |
|
575 |
`(:crossrefs |
|
576 |
,(org-publish-cache-get-file-property |
|
577 |
;; Normalize file names in cache. |
|
578 |
(file-truename filename) :crossrefs nil t) |
|
579 |
:filter-final-output |
|
580 |
(org-publish--store-crossrefs |
|
581 |
org-publish-collect-index |
|
582 |
,@(plist-get plist :filter-final-output))))))) |
|
583 |
;; Remove opened buffer in the process. |
|
584 |
(unless visiting (kill-buffer work-buffer))))) |
|
585 |
|
|
586 |
(defun org-publish-attachment (_plist filename pub-dir) |
|
587 |
"Publish a file with no transformation of any kind. |
|
588 |
|
|
589 |
FILENAME is the filename of the Org file to be published. PLIST |
|
590 |
is the property list for the given project. PUB-DIR is the |
|
591 |
publishing directory. |
|
592 |
|
|
593 |
Return output file name." |
|
594 |
(unless (file-directory-p pub-dir) |
|
595 |
(make-directory pub-dir t)) |
|
596 |
(let ((output (expand-file-name (file-name-nondirectory filename) pub-dir))) |
|
597 |
(unless (file-equal-p (expand-file-name (file-name-directory filename)) |
|
598 |
(file-name-as-directory (expand-file-name pub-dir))) |
|
599 |
(copy-file filename output t)) |
|
600 |
;; Return file name. |
|
601 |
output)) |
|
602 |
|
|
603 |
|
|
604 |
|
|
605 |
;;; Publishing files, sets of files |
|
606 |
|
|
607 |
(defun org-publish-file (filename &optional project no-cache) |
|
608 |
"Publish file FILENAME from PROJECT. |
|
609 |
If NO-CACHE is not nil, do not initialize `org-publish-cache'. |
|
610 |
This is needed, since this function is used to publish single |
|
611 |
files, when entire projects are published (see |
|
612 |
`org-publish-projects')." |
|
613 |
(let* ((project |
|
614 |
(or project |
|
615 |
(org-publish-get-project-from-filename filename) |
|
616 |
(user-error "File %S is not part of any known project" |
|
617 |
(abbreviate-file-name filename)))) |
|
618 |
(project-plist (cdr project)) |
|
619 |
(publishing-function |
|
620 |
(pcase (org-publish-property :publishing-function project) |
|
621 |
(`nil (user-error "No publishing function chosen")) |
|
622 |
((and f (pred listp)) f) |
|
623 |
(f (list f)))) |
|
624 |
(base-dir |
|
625 |
(file-name-as-directory |
|
626 |
(or (org-publish-property :base-directory project) |
|
627 |
(user-error "Project %S does not have :base-directory defined" |
|
628 |
(car project))))) |
|
629 |
(pub-base-dir |
|
630 |
(file-name-as-directory |
|
631 |
(or (org-publish-property :publishing-directory project) |
|
632 |
(user-error |
|
633 |
"Project %S does not have :publishing-directory defined" |
|
634 |
(car project))))) |
|
635 |
(pub-dir |
|
636 |
(file-name-directory |
|
637 |
(expand-file-name (file-relative-name filename base-dir) |
|
638 |
pub-base-dir)))) |
|
639 |
|
|
640 |
(unless no-cache (org-publish-initialize-cache (car project))) |
|
641 |
|
|
642 |
;; Allow chain of publishing functions. |
|
643 |
(dolist (f publishing-function) |
|
644 |
(when (org-publish-needed-p filename pub-base-dir f pub-dir base-dir) |
|
645 |
(let ((output (funcall f project-plist filename pub-dir))) |
|
646 |
(org-publish-update-timestamp filename pub-base-dir f base-dir) |
|
647 |
(run-hook-with-args 'org-publish-after-publishing-hook |
|
648 |
filename |
|
649 |
output)))) |
|
650 |
;; Make sure to write cache to file after successfully publishing |
|
651 |
;; a file, so as to minimize impact of a publishing failure. |
|
652 |
(org-publish-write-cache-file))) |
|
653 |
|
|
654 |
(defun org-publish-projects (projects) |
|
655 |
"Publish all files belonging to the PROJECTS alist. |
|
656 |
If `:auto-sitemap' is set, publish the sitemap too. If |
|
657 |
`:makeindex' is set, also produce a file \"theindex.org\"." |
|
658 |
(dolist (project (org-publish-expand-projects projects)) |
|
659 |
(let ((plist (cdr project))) |
|
660 |
(let ((fun (org-publish-property :preparation-function project))) |
|
661 |
(cond |
|
662 |
((consp fun) (dolist (f fun) (funcall f plist))) |
|
663 |
((functionp fun) (funcall fun plist)))) |
|
664 |
;; Each project uses its own cache file. |
|
665 |
(org-publish-initialize-cache (car project)) |
|
666 |
(when (org-publish-property :auto-sitemap project) |
|
667 |
(let ((sitemap-filename |
|
668 |
(or (org-publish-property :sitemap-filename project) |
|
669 |
"sitemap.org"))) |
|
670 |
(org-publish-sitemap project sitemap-filename))) |
|
671 |
;; Publish all files from PROJECT except "theindex.org". Its |
|
672 |
;; publishing will be deferred until "theindex.inc" is |
|
673 |
;; populated. |
|
674 |
(let ((theindex |
|
675 |
(expand-file-name "theindex.org" |
|
676 |
(org-publish-property :base-directory project)))) |
|
677 |
(dolist (file (org-publish-get-base-files project)) |
|
678 |
(unless (file-equal-p file theindex) |
|
679 |
(org-publish-file file project t))) |
|
680 |
;; Populate "theindex.inc", if needed, and publish |
|
681 |
;; "theindex.org". |
|
682 |
(when (org-publish-property :makeindex project) |
|
683 |
(org-publish-index-generate-theindex |
|
684 |
project (org-publish-property :base-directory project)) |
|
685 |
(org-publish-file theindex project t))) |
|
686 |
(let ((fun (org-publish-property :completion-function project))) |
|
687 |
(cond |
|
688 |
((consp fun) (dolist (f fun) (funcall f plist))) |
|
689 |
((functionp fun) (funcall fun plist))))) |
|
690 |
(org-publish-write-cache-file))) |
|
691 |
|
|
692 |
|
|
693 |
;;; Site map generation |
|
694 |
|
|
695 |
(defun org-publish--sitemap-files-to-lisp (files project style format-entry) |
|
696 |
"Represent FILES as a parsed plain list. |
|
697 |
FILES is the list of files in the site map. PROJECT is the |
|
698 |
current project. STYLE determines is either `list' or `tree'. |
|
699 |
FORMAT-ENTRY is a function called on each file which should |
|
700 |
return a string. Return value is a list as returned by |
|
701 |
`org-list-to-lisp'." |
|
702 |
(let ((root (expand-file-name |
|
703 |
(file-name-as-directory |
|
704 |
(org-publish-property :base-directory project))))) |
|
705 |
(pcase style |
|
706 |
(`list |
|
707 |
(cons 'unordered |
|
708 |
(mapcar |
|
709 |
(lambda (f) |
|
710 |
(list (funcall format-entry |
|
711 |
(file-relative-name f root) |
|
712 |
style |
|
713 |
project))) |
|
714 |
files))) |
|
715 |
(`tree |
|
716 |
(letrec ((files-only (cl-remove-if #'directory-name-p files)) |
|
717 |
(directories (cl-remove-if-not #'directory-name-p files)) |
|
718 |
(subtree-to-list |
|
719 |
(lambda (dir) |
|
720 |
(cons 'unordered |
|
721 |
(nconc |
|
722 |
;; Files in DIR. |
|
723 |
(mapcar |
|
724 |
(lambda (f) |
|
725 |
(list (funcall format-entry |
|
726 |
(file-relative-name f root) |
|
727 |
style |
|
728 |
project))) |
|
729 |
(cl-remove-if-not |
|
730 |
(lambda (f) (string= dir (file-name-directory f))) |
|
731 |
files-only)) |
|
732 |
;; Direct sub-directories. |
|
733 |
(mapcar |
|
734 |
(lambda (sub) |
|
735 |
(list (funcall format-entry |
|
736 |
(file-relative-name sub root) |
|
737 |
style |
|
738 |
project) |
|
739 |
(funcall subtree-to-list sub))) |
|
740 |
(cl-remove-if-not |
|
741 |
(lambda (f) |
|
742 |
(string= |
|
743 |
dir |
|
744 |
;; Parent directory. |
|
745 |
(file-name-directory (directory-file-name f)))) |
|
746 |
directories))))))) |
|
747 |
(funcall subtree-to-list root))) |
|
748 |
(_ (user-error "Unknown site-map style: `%s'" style))))) |
|
749 |
|
|
750 |
(defun org-publish-sitemap (project &optional sitemap-filename) |
|
751 |
"Create a sitemap of pages in set defined by PROJECT. |
|
752 |
Optionally set the filename of the sitemap with SITEMAP-FILENAME. |
|
753 |
Default for SITEMAP-FILENAME is `sitemap.org'." |
|
754 |
(let* ((root (expand-file-name |
|
755 |
(file-name-as-directory |
|
756 |
(org-publish-property :base-directory project)))) |
|
757 |
(sitemap-filename (concat root (or sitemap-filename "sitemap.org"))) |
|
758 |
(title (or (org-publish-property :sitemap-title project) |
|
759 |
(concat "Sitemap for project " (car project)))) |
|
760 |
(style (or (org-publish-property :sitemap-style project) |
|
761 |
'tree)) |
|
762 |
(sitemap-builder (or (org-publish-property :sitemap-function project) |
|
763 |
#'org-publish-sitemap-default)) |
|
764 |
(format-entry (or (org-publish-property :sitemap-format-entry project) |
|
765 |
#'org-publish-sitemap-default-entry)) |
|
766 |
(sort-folders |
|
767 |
(org-publish-property :sitemap-sort-folders project |
|
768 |
org-publish-sitemap-sort-folders)) |
|
769 |
(sort-files |
|
770 |
(org-publish-property :sitemap-sort-files project |
|
771 |
org-publish-sitemap-sort-files)) |
|
772 |
(ignore-case |
|
773 |
(org-publish-property :sitemap-ignore-case project |
|
774 |
org-publish-sitemap-sort-ignore-case)) |
|
775 |
(org-file-p (lambda (f) (equal "org" (file-name-extension f)))) |
|
776 |
(sort-predicate |
|
777 |
(lambda (a b) |
|
778 |
(let ((retval t)) |
|
779 |
;; First we sort files: |
|
780 |
(pcase sort-files |
|
781 |
(`alphabetically |
|
782 |
(let ((A (if (funcall org-file-p a) |
|
783 |
(concat (file-name-directory a) |
|
784 |
(org-publish-find-title a project)) |
|
785 |
a)) |
|
786 |
(B (if (funcall org-file-p b) |
|
787 |
(concat (file-name-directory b) |
|
788 |
(org-publish-find-title b project)) |
|
789 |
b))) |
|
790 |
(setq retval |
|
791 |
(if ignore-case |
|
792 |
(not (string-lessp (upcase B) (upcase A))) |
|
793 |
(not (string-lessp B A)))))) |
|
794 |
((or `anti-chronologically `chronologically) |
|
795 |
(let* ((adate (org-publish-find-date a project)) |
|
796 |
(bdate (org-publish-find-date b project)) |
|
797 |
(A (+ (lsh (car adate) 16) (cadr adate))) |
|
798 |
(B (+ (lsh (car bdate) 16) (cadr bdate)))) |
|
799 |
(setq retval |
|
800 |
(if (eq sort-files 'chronologically) |
|
801 |
(<= A B) |
|
802 |
(>= A B))))) |
|
803 |
(`nil nil) |
|
804 |
(_ (user-error "Invalid sort value %s" sort-files))) |
|
805 |
;; Directory-wise wins: |
|
806 |
(when (memq sort-folders '(first last)) |
|
807 |
;; a is directory, b not: |
|
808 |
(cond |
|
809 |
((and (file-directory-p a) (not (file-directory-p b))) |
|
810 |
(setq retval (eq sort-folders 'first))) |
|
811 |
;; a is not a directory, but b is: |
|
812 |
((and (not (file-directory-p a)) (file-directory-p b)) |
|
813 |
(setq retval (eq sort-folders 'last))))) |
|
814 |
retval)))) |
|
815 |
(message "Generating sitemap for %s" title) |
|
816 |
(with-temp-file sitemap-filename |
|
817 |
(insert |
|
818 |
(let ((files (remove sitemap-filename |
|
819 |
(org-publish-get-base-files project)))) |
|
820 |
;; Add directories, if applicable. |
|
821 |
(unless (and (eq style 'list) (eq sort-folders 'ignore)) |
|
822 |
(setq files |
|
823 |
(nconc (remove root (org-uniquify |
|
824 |
(mapcar #'file-name-directory files))) |
|
825 |
files))) |
|
826 |
;; Eventually sort all entries. |
|
827 |
(when (or sort-files (not (memq sort-folders 'ignore))) |
|
828 |
(setq files (sort files sort-predicate))) |
|
829 |
(funcall sitemap-builder |
|
830 |
title |
|
831 |
(org-publish--sitemap-files-to-lisp |
|
832 |
files project style format-entry))))))) |
|
833 |
|
|
834 |
(defun org-publish-find-property (file property project &optional backend) |
|
835 |
"Find the PROPERTY of FILE in project. |
|
836 |
|
|
837 |
PROPERTY is a keyword referring to an export option, as defined |
|
838 |
in `org-export-options-alist' or in export back-ends. In the |
|
839 |
latter case, optional argument BACKEND has to be set to the |
|
840 |
back-end where the option is defined, e.g., |
|
841 |
|
|
842 |
(org-publish-find-property file :subtitle 'latex) |
|
843 |
|
|
844 |
Return value may be a string or a list, depending on the type of |
|
845 |
PROPERTY, i.e. \"behavior\" parameter from `org-export-options-alist'." |
|
846 |
(let ((file (org-publish--expand-file-name file project))) |
|
847 |
(when (and (file-readable-p file) (not (directory-name-p file))) |
|
848 |
(let* ((org-inhibit-startup t) |
|
849 |
(visiting (find-buffer-visiting file)) |
|
850 |
(buffer (or visiting (find-file-noselect file)))) |
|
851 |
(unwind-protect |
|
852 |
(plist-get (with-current-buffer buffer |
|
853 |
(if (not visiting) (org-export-get-environment backend) |
|
854 |
;; Protect local variables in open buffers. |
|
855 |
(org-export-with-buffer-copy |
|
856 |
(org-export-get-environment backend)))) |
|
857 |
property) |
|
858 |
(unless visiting (kill-buffer buffer))))))) |
|
859 |
|
|
860 |
(defun org-publish-find-title (file project) |
|
861 |
"Find the title of FILE in PROJECT." |
|
862 |
(let ((file (org-publish--expand-file-name file project))) |
|
863 |
(or (org-publish-cache-get-file-property file :title nil t) |
|
864 |
(let* ((parsed-title (org-publish-find-property file :title project)) |
|
865 |
(title |
|
866 |
(if parsed-title |
|
867 |
;; Remove property so that the return value is |
|
868 |
;; cache-able (i.e., it can be `read' back). |
|
869 |
(org-no-properties |
|
870 |
(org-element-interpret-data parsed-title)) |
|
871 |
(file-name-nondirectory (file-name-sans-extension file))))) |
|
872 |
(org-publish-cache-set-file-property file :title title) |
|
873 |
title)))) |
|
874 |
|
|
875 |
(defun org-publish-find-date (file project) |
|
876 |
"Find the date of FILE in PROJECT. |
|
877 |
This function assumes FILE is either a directory or an Org file. |
|
878 |
If FILE is an Org file and provides a DATE keyword use it. In |
|
879 |
any other case use the file system's modification time. Return |
|
880 |
time in `current-time' format." |
|
881 |
(let ((file (org-publish--expand-file-name file project))) |
|
882 |
(if (file-directory-p file) (nth 5 (file-attributes file)) |
|
883 |
(let ((date (org-publish-find-property file :date project))) |
|
884 |
;; DATE is a secondary string. If it contains a time-stamp, |
|
885 |
;; convert it to internal format. Otherwise, use FILE |
|
886 |
;; modification time. |
|
887 |
(cond ((let ((ts (and (consp date) (assq 'timestamp date)))) |
|
888 |
(and ts |
|
889 |
(let ((value (org-element-interpret-data ts))) |
|
890 |
(and (org-string-nw-p value) |
|
891 |
(org-time-string-to-time value)))))) |
|
892 |
((file-exists-p file) (nth 5 (file-attributes file))) |
|
893 |
(t (error "No such file: \"%s\"" file))))))) |
|
894 |
|
|
895 |
(defun org-publish-sitemap-default-entry (entry style project) |
|
896 |
"Default format for site map ENTRY, as a string. |
|
897 |
ENTRY is a file name. STYLE is the style of the sitemap. |
|
898 |
PROJECT is the current project." |
|
899 |
(cond ((not (directory-name-p entry)) |
|
900 |
(format "[[file:%s][%s]]" |
|
901 |
entry |
|
902 |
(org-publish-find-title entry project))) |
|
903 |
((eq style 'tree) |
|
904 |
;; Return only last subdir. |
|
905 |
(file-name-nondirectory (directory-file-name entry))) |
|
906 |
(t entry))) |
|
907 |
|
|
908 |
(defun org-publish-sitemap-default (title list) |
|
909 |
"Default site map, as a string. |
|
910 |
TITLE is the the title of the site map. LIST is an internal |
|
911 |
representation for the files to include, as returned by |
|
912 |
`org-list-to-lisp'. PROJECT is the current project." |
|
913 |
(concat "#+TITLE: " title "\n\n" |
|
914 |
(org-list-to-org list))) |
|
915 |
|
|
916 |
|
|
917 |
;;; Interactive publishing functions |
|
918 |
|
|
919 |
;;;###autoload |
|
920 |
(defalias 'org-publish-project 'org-publish) |
|
921 |
|
|
922 |
;;;###autoload |
|
923 |
(defun org-publish (project &optional force async) |
|
924 |
"Publish PROJECT. |
|
925 |
|
|
926 |
PROJECT is either a project name, as a string, or a project |
|
927 |
alist (see `org-publish-project-alist' variable). |
|
928 |
|
|
929 |
When optional argument FORCE is non-nil, force publishing all |
|
930 |
files in PROJECT. With a non-nil optional argument ASYNC, |
|
931 |
publishing will be done asynchronously, in another process." |
|
932 |
(interactive |
|
933 |
(list (assoc (completing-read "Publish project: " |
|
934 |
org-publish-project-alist nil t) |
|
935 |
org-publish-project-alist) |
|
936 |
current-prefix-arg)) |
|
937 |
(let ((project (if (not (stringp project)) project |
|
938 |
;; If this function is called in batch mode, |
|
939 |
;; PROJECT is still a string here. |
|
940 |
(assoc project org-publish-project-alist)))) |
|
941 |
(cond |
|
942 |
((not project)) |
|
943 |
(async |
|
944 |
(org-export-async-start (lambda (_) nil) |
|
945 |
`(let ((org-publish-use-timestamps-flag |
|
946 |
,(and (not force) org-publish-use-timestamps-flag))) |
|
947 |
;; Expand components right now as external process may not |
|
948 |
;; be aware of complete `org-publish-project-alist'. |
|
949 |
(org-publish-projects |
|
950 |
',(org-publish-expand-projects (list project)))))) |
|
951 |
(t (save-window-excursion |
|
952 |
(let ((org-publish-use-timestamps-flag |
|
953 |
(and (not force) org-publish-use-timestamps-flag))) |
|
954 |
(org-publish-projects (list project)))))))) |
|
955 |
|
|
956 |
;;;###autoload |
|
957 |
(defun org-publish-all (&optional force async) |
|
958 |
"Publish all projects. |
|
959 |
With prefix argument FORCE, remove all files in the timestamp |
|
960 |
directory and force publishing all projects. With a non-nil |
|
961 |
optional argument ASYNC, publishing will be done asynchronously, |
|
962 |
in another process." |
|
963 |
(interactive "P") |
|
964 |
(if async |
|
965 |
(org-export-async-start (lambda (_) nil) |
|
966 |
`(progn |
|
967 |
(when ',force (org-publish-remove-all-timestamps)) |
|
968 |
(let ((org-publish-use-timestamps-flag |
|
969 |
(if ',force nil ,org-publish-use-timestamps-flag))) |
|
970 |
(org-publish-projects ',org-publish-project-alist)))) |
|
971 |
(when force (org-publish-remove-all-timestamps)) |
|
972 |
(save-window-excursion |
|
973 |
(let ((org-publish-use-timestamps-flag |
|
974 |
(if force nil org-publish-use-timestamps-flag))) |
|
975 |
(org-publish-projects org-publish-project-alist))))) |
|
976 |
|
|
977 |
|
|
978 |
;;;###autoload |
|
979 |
(defun org-publish-current-file (&optional force async) |
|
980 |
"Publish the current file. |
|
981 |
With prefix argument FORCE, force publish the file. When |
|
982 |
optional argument ASYNC is non-nil, publishing will be done |
|
983 |
asynchronously, in another process." |
|
984 |
(interactive "P") |
|
985 |
(let ((file (buffer-file-name (buffer-base-buffer)))) |
|
986 |
(if async |
|
987 |
(org-export-async-start (lambda (_) nil) |
|
988 |
`(let ((org-publish-use-timestamps-flag |
|
989 |
(if ',force nil ,org-publish-use-timestamps-flag))) |
|
990 |
(org-publish-file ,file))) |
|
991 |
(save-window-excursion |
|
992 |
(let ((org-publish-use-timestamps-flag |
|
993 |
(if force nil org-publish-use-timestamps-flag))) |
|
994 |
(org-publish-file file)))))) |
|
995 |
|
|
996 |
;;;###autoload |
|
997 |
(defun org-publish-current-project (&optional force async) |
|
998 |
"Publish the project associated with the current file. |
|
999 |
With a prefix argument, force publishing of all files in |
|
1000 |
the project." |
|
1001 |
(interactive "P") |
|
1002 |
(save-window-excursion |
|
1003 |
(let ((project (org-publish-get-project-from-filename |
|
1004 |
(buffer-file-name (buffer-base-buffer)) 'up))) |
|
1005 |
(if project (org-publish project force async) |
|
1006 |
(error "File %s is not part of any known project" |
|
1007 |
(buffer-file-name (buffer-base-buffer))))))) |
|
1008 |
|
|
1009 |
|
|
1010 |
|
|
1011 |
;;; Index generation |
|
1012 |
|
|
1013 |
(defun org-publish-collect-index (output _backend info) |
|
1014 |
"Update index for a file in cache. |
|
1015 |
|
|
1016 |
OUTPUT is the output from transcoding current file. BACKEND is |
|
1017 |
the back-end that was used for transcoding. INFO is a plist |
|
1018 |
containing publishing and export options. |
|
1019 |
|
|
1020 |
The index relative to current file is stored as an alist. An |
|
1021 |
association has the following shape: (TERM FILE-NAME PARENT), |
|
1022 |
where TERM is the indexed term, as a string, FILE-NAME is the |
|
1023 |
original full path of the file where the term in encountered, and |
|
1024 |
PARENT is a reference to the headline, if any, containing the |
|
1025 |
original index keyword. When non-nil, this reference is a cons |
|
1026 |
cell. Its CAR is a symbol among `id', `custom-id' and `name' and |
|
1027 |
its CDR is a string." |
|
1028 |
(let ((file (file-truename (plist-get info :input-file)))) |
|
1029 |
(org-publish-cache-set-file-property |
|
1030 |
file :index |
|
1031 |
(delete-dups |
|
1032 |
(org-element-map (plist-get info :parse-tree) 'keyword |
|
1033 |
(lambda (k) |
|
1034 |
(when (equal (org-element-property :key k) "INDEX") |
|
1035 |
(let ((parent (org-export-get-parent-headline k))) |
|
1036 |
(list (org-element-property :value k) |
|
1037 |
file |
|
1038 |
(cond |
|
1039 |
((not parent) nil) |
|
1040 |
((let ((id (org-element-property :ID parent))) |
|
1041 |
(and id (cons 'id id)))) |
|
1042 |
((let ((id (org-element-property :CUSTOM_ID parent))) |
|
1043 |
(and id (cons 'custom-id id)))) |
|
1044 |
(t (cons 'name |
|
1045 |
;; Remove statistics cookie. |
|
1046 |
(replace-regexp-in-string |
|
1047 |
"\\[[0-9]+%\\]\\|\\[[0-9]+/[0-9]+\\]" "" |
|
1048 |
(org-element-property :raw-value parent))))))))) |
|
1049 |
info)))) |
|
1050 |
;; Return output unchanged. |
|
1051 |
output) |
|
1052 |
|
|
1053 |
(defun org-publish-index-generate-theindex (project directory) |
|
1054 |
"Retrieve full index from cache and build \"theindex.org\". |
|
1055 |
PROJECT is the project the index relates to. DIRECTORY is the |
|
1056 |
publishing directory." |
|
1057 |
(let ((all-files (org-publish-get-base-files project)) |
|
1058 |
full-index) |
|
1059 |
;; Compile full index and sort it alphabetically. |
|
1060 |
(dolist (file all-files |
|
1061 |
(setq full-index |
|
1062 |
(sort (nreverse full-index) |
|
1063 |
(lambda (a b) (string< (downcase (car a)) |
|
1064 |
(downcase (car b))))))) |
|
1065 |
(let ((index (org-publish-cache-get-file-property file :index))) |
|
1066 |
(dolist (term index) |
|
1067 |
(unless (member term full-index) (push term full-index))))) |
|
1068 |
;; Write "theindex.inc" in DIRECTORY. |
|
1069 |
(with-temp-file (expand-file-name "theindex.inc" directory) |
|
1070 |
(let ((current-letter nil) (last-entry nil)) |
|
1071 |
(dolist (idx full-index) |
|
1072 |
(let* ((entry (org-split-string (car idx) "!")) |
|
1073 |
(letter (upcase (substring (car entry) 0 1))) |
|
1074 |
;; Transform file into a path relative to publishing |
|
1075 |
;; directory. |
|
1076 |
(file (file-relative-name |
|
1077 |
(nth 1 idx) |
|
1078 |
(plist-get (cdr project) :base-directory)))) |
|
1079 |
;; Check if another letter has to be inserted. |
|
1080 |
(unless (string= letter current-letter) |
|
1081 |
(insert (format "* %s\n" letter))) |
|
1082 |
;; Compute the first difference between last entry and |
|
1083 |
;; current one: it tells the level at which new items |
|
1084 |
;; should be added. |
|
1085 |
(let* ((rank |
|
1086 |
(if (equal entry last-entry) (1- (length entry)) |
|
1087 |
(cl-loop for n from 0 to (length entry) |
|
1088 |
unless (equal (nth n entry) (nth n last-entry)) |
|
1089 |
return n))) |
|
1090 |
(len (length (nthcdr rank entry)))) |
|
1091 |
;; For each term after the first difference, create |
|
1092 |
;; a new sub-list with the term as body. Moreover, |
|
1093 |
;; linkify the last term. |
|
1094 |
(dotimes (n len) |
|
1095 |
(insert |
|
1096 |
(concat |
|
1097 |
(make-string (* (+ rank n) 2) ?\s) " - " |
|
1098 |
(if (not (= (1- len) n)) (nth (+ rank n) entry) |
|
1099 |
;; Last term: Link it to TARGET, if possible. |
|
1100 |
(let ((target (nth 2 idx))) |
|
1101 |
(format |
|
1102 |
"[[%s][%s]]" |
|
1103 |
;; Destination. |
|
1104 |
(pcase (car target) |
|
1105 |
(`nil (format "file:%s" file)) |
|
1106 |
(`id (format "id:%s" (cdr target))) |
|
1107 |
(`custom-id (format "file:%s::#%s" file (cdr target))) |
|
1108 |
(_ (format "file:%s::*%s" file (cdr target)))) |
|
1109 |
;; Description. |
|
1110 |
(car (last entry))))) |
|
1111 |
"\n")))) |
|
1112 |
(setq current-letter letter last-entry entry)))) |
|
1113 |
;; Create "theindex.org", if it doesn't exist yet, and provide |
|
1114 |
;; a default index file. |
|
1115 |
(let ((index.org (expand-file-name "theindex.org" directory))) |
|
1116 |
(unless (file-exists-p index.org) |
|
1117 |
(with-temp-file index.org |
|
1118 |
(insert "#+TITLE: Index\n\n#+INCLUDE: \"theindex.inc\"\n\n"))))))) |
|
1119 |
|
|
1120 |
|
|
1121 |
|
|
1122 |
;;; External Fuzzy Links Resolution |
|
1123 |
;; |
|
1124 |
;; This part implements tools to resolve [[file.org::*Some headline]] |
|
1125 |
;; links, where "file.org" belongs to the current project. |
|
1126 |
|
|
1127 |
(defun org-publish--store-crossrefs (output _backend info) |
|
1128 |
"Store cross-references for current published file. |
|
1129 |
|
|
1130 |
OUTPUT is the produced output, as a string. BACKEND is the export |
|
1131 |
back-end used, as a symbol. INFO is the final export state, as |
|
1132 |
a plist. |
|
1133 |
|
|
1134 |
This function is meant to be used as a final output filter. See |
|
1135 |
`org-publish-org-to'." |
|
1136 |
(org-publish-cache-set-file-property |
|
1137 |
(file-truename (plist-get info :input-file)) |
|
1138 |
:crossrefs |
|
1139 |
;; Update `:crossrefs' so as to remove unused references and search |
|
1140 |
;; cells. Actually used references are extracted from |
|
1141 |
;; `:internal-references', with references as strings removed. See |
|
1142 |
;; `org-export-get-reference' for details. |
|
1143 |
(cl-remove-if (lambda (pair) (stringp (car pair))) |
|
1144 |
(plist-get info :internal-references))) |
|
1145 |
;; Return output unchanged. |
|
1146 |
output) |
|
1147 |
|
|
1148 |
(defun org-publish-resolve-external-link (search file) |
|
1149 |
"Return reference for element matching string SEARCH in FILE. |
|
1150 |
|
|
1151 |
Return value is an internal reference, as a string. |
|
1152 |
|
|
1153 |
This function allows resolving external links with a search |
|
1154 |
option, e.g., |
|
1155 |
|
|
1156 |
[[file.org::*heading][description]] |
|
1157 |
[[file.org::#custom-id][description]] |
|
1158 |
[[file.org::fuzzy][description]] |
|
1159 |
|
|
1160 |
It only makes sense to use this if export back-end builds |
|
1161 |
references with `org-export-get-reference'." |
|
1162 |
(if (not org-publish-cache) |
|
1163 |
(progn |
|
1164 |
(message "Reference %S in file %S cannot be resolved without publishing" |
|
1165 |
search |
|
1166 |
file) |
|
1167 |
"MissingReference") |
|
1168 |
(let* ((filename (file-truename file)) |
|
1169 |
(crossrefs |
|
1170 |
(org-publish-cache-get-file-property filename :crossrefs nil t)) |
|
1171 |
(cells |
|
1172 |
(org-export-string-to-search-cell (org-link-unescape search)))) |
|
1173 |
(or |
|
1174 |
;; Look for reference associated to search cells triggered by |
|
1175 |
;; LINK. It can match when targeted file has been published |
|
1176 |
;; already. |
|
1177 |
(let ((known (cdr (cl-some (lambda (c) (assoc c crossrefs)) cells)))) |
|
1178 |
(and known (org-export-format-reference known))) |
|
1179 |
;; Search cell is unknown so far. Generate a new internal |
|
1180 |
;; reference that will be used when the targeted file will be |
|
1181 |
;; published. |
|
1182 |
(let ((new (org-export-new-reference crossrefs))) |
|
1183 |
(dolist (cell cells) (push (cons cell new) crossrefs)) |
|
1184 |
(org-publish-cache-set-file-property filename :crossrefs crossrefs) |
|
1185 |
(org-export-format-reference new)))))) |
|
1186 |
|
|
1187 |
(defun org-publish-file-relative-name (filename info) |
|
1188 |
"Convert FILENAME to be relative to current project's base directory. |
|
1189 |
INFO is the plist containing the current export state. The |
|
1190 |
function does not change relative file names." |
|
1191 |
(let ((base (plist-get info :base-directory))) |
|
1192 |
(if (and base |
|
1193 |
(file-name-absolute-p filename) |
|
1194 |
(file-in-directory-p filename base)) |
|
1195 |
(file-relative-name filename base) |
|
1196 |
filename))) |
|
1197 |
|
|
1198 |
|
|
1199 |
|
|
1200 |
;;; Caching functions |
|
1201 |
|
|
1202 |
(defun org-publish-write-cache-file (&optional free-cache) |
|
1203 |
"Write `org-publish-cache' to file. |
|
1204 |
If FREE-CACHE, empty the cache." |
|
1205 |
(unless org-publish-cache |
|
1206 |
(error "`org-publish-write-cache-file' called, but no cache present")) |
|
1207 |
|
|
1208 |
(let ((cache-file (org-publish-cache-get ":cache-file:"))) |
|
1209 |
(unless cache-file |
|
1210 |
(error "Cannot find cache-file name in `org-publish-write-cache-file'")) |
|
1211 |
(with-temp-file cache-file |
|
1212 |
(let (print-level print-length) |
|
1213 |
(insert "(setq org-publish-cache \ |
|
1214 |
\(make-hash-table :test 'equal :weakness nil :size 100))\n") |
|
1215 |
(maphash (lambda (k v) |
|
1216 |
(insert |
|
1217 |
(format "(puthash %S %s%S org-publish-cache)\n" |
|
1218 |
k (if (or (listp v) (symbolp v)) "'" "") v))) |
|
1219 |
org-publish-cache))) |
|
1220 |
(when free-cache (org-publish-reset-cache)))) |
|
1221 |
|
|
1222 |
(defun org-publish-initialize-cache (project-name) |
|
1223 |
"Initialize the projects cache if not initialized yet and return it." |
|
1224 |
|
|
1225 |
(unless project-name |
|
1226 |
(error "Cannot initialize `org-publish-cache' without projects name in \ |
|
1227 |
`org-publish-initialize-cache'")) |
|
1228 |
|
|
1229 |
(unless (file-exists-p org-publish-timestamp-directory) |
|
1230 |
(make-directory org-publish-timestamp-directory t)) |
|
1231 |
(unless (file-directory-p org-publish-timestamp-directory) |
|
1232 |
(error "Org publish timestamp: %s is not a directory" |
|
1233 |
org-publish-timestamp-directory)) |
|
1234 |
|
|
1235 |
(unless (and org-publish-cache |
|
1236 |
(string= (org-publish-cache-get ":project:") project-name)) |
|
1237 |
(let* ((cache-file |
|
1238 |
(concat |
|
1239 |
(expand-file-name org-publish-timestamp-directory) |
|
1240 |
project-name ".cache")) |
|
1241 |
(cexists (file-exists-p cache-file))) |
|
1242 |
|
|
1243 |
(when org-publish-cache (org-publish-reset-cache)) |
|
1244 |
|
|
1245 |
(if cexists (load-file cache-file) |
|
1246 |
(setq org-publish-cache |
|
1247 |
(make-hash-table :test 'equal :weakness nil :size 100)) |
|
1248 |
(org-publish-cache-set ":project:" project-name) |
|
1249 |
(org-publish-cache-set ":cache-file:" cache-file)) |
|
1250 |
(unless cexists (org-publish-write-cache-file nil)))) |
|
1251 |
org-publish-cache) |
|
1252 |
|
|
1253 |
(defun org-publish-reset-cache () |
|
1254 |
"Empty org-publish-cache and reset it nil." |
|
1255 |
(message "%s" "Resetting org-publish-cache") |
|
1256 |
(when (hash-table-p org-publish-cache) |
|
1257 |
(clrhash org-publish-cache)) |
|
1258 |
(setq org-publish-cache nil)) |
|
1259 |
|
|
1260 |
(defun org-publish-cache-file-needs-publishing |
|
1261 |
(filename &optional pub-dir pub-func _base-dir) |
|
1262 |
"Check the timestamp of the last publishing of FILENAME. |
|
1263 |
Return non-nil if the file needs publishing. Also check if |
|
1264 |
any included files have been more recently published, so that |
|
1265 |
the file including them will be republished as well." |
|
1266 |
(unless org-publish-cache |
|
1267 |
(error |
|
1268 |
"`org-publish-cache-file-needs-publishing' called, but no cache present")) |
|
1269 |
(let* ((key (org-publish-timestamp-filename filename pub-dir pub-func)) |
|
1270 |
(pstamp (org-publish-cache-get key)) |
|
1271 |
(org-inhibit-startup t) |
|
1272 |
included-files-ctime) |
|
1273 |
(when (equal (file-name-extension filename) "org") |
|
1274 |
(let ((visiting (find-buffer-visiting filename)) |
|
1275 |
(buf (find-file-noselect filename)) |
|
1276 |
(case-fold-search t)) |
|
1277 |
(unwind-protect |
|
1278 |
(with-current-buffer buf |
|
1279 |
(goto-char (point-min)) |
|
1280 |
(while (re-search-forward "^[ \t]*#\\+INCLUDE:" nil t) |
|
1281 |
(let ((element (org-element-at-point))) |
|
1282 |
(when (eq 'keyword (org-element-type element)) |
|
1283 |
(let* ((value (org-element-property :value element)) |
|
1284 |
(filename |
|
1285 |
(and (string-match "\\`\\(\".+?\"\\|\\S-+\\)" value) |
|
1286 |
(let ((m (org-unbracket-string |
|
1287 |
"\"" "\"" (match-string 1 value)))) |
|
1288 |
;; Ignore search suffix. |
|
1289 |
(if (string-match "::.*?\\'" m) |
|
1290 |
(substring m 0 (match-beginning 0)) |
|
1291 |
m))))) |
|
1292 |
(when filename |
|
1293 |
(push (org-publish-cache-ctime-of-src |
|
1294 |
(expand-file-name filename)) |
|
1295 |
included-files-ctime))))))) |
|
1296 |
(unless visiting (kill-buffer buf))))) |
|
1297 |
(or (null pstamp) |
|
1298 |
(let ((ctime (org-publish-cache-ctime-of-src filename))) |
|
1299 |
(or (< pstamp ctime) |
|
1300 |
(cl-some (lambda (ct) (< ctime ct)) included-files-ctime)))))) |
|
1301 |
|
|
1302 |
(defun org-publish-cache-set-file-property |
|
1303 |
(filename property value &optional project-name) |
|
1304 |
"Set the VALUE for a PROPERTY of file FILENAME in publishing cache to VALUE. |
|
1305 |
Use cache file of PROJECT-NAME. If the entry does not exist, it |
|
1306 |
will be created. Return VALUE." |
|
1307 |
;; Evtl. load the requested cache file: |
|
1308 |
(if project-name (org-publish-initialize-cache project-name)) |
|
1309 |
(let ((pl (org-publish-cache-get filename))) |
|
1310 |
(if pl (progn (plist-put pl property value) value) |
|
1311 |
(org-publish-cache-get-file-property |
|
1312 |
filename property value nil project-name)))) |
|
1313 |
|
|
1314 |
(defun org-publish-cache-get-file-property |
|
1315 |
(filename property &optional default no-create project-name) |
|
1316 |
"Return the value for a PROPERTY of file FILENAME in publishing cache. |
|
1317 |
Use cache file of PROJECT-NAME. Return the value of that PROPERTY, |
|
1318 |
or DEFAULT, if the value does not yet exist. Create the entry, |
|
1319 |
if necessary, unless NO-CREATE is non-nil." |
|
1320 |
(when project-name (org-publish-initialize-cache project-name)) |
|
1321 |
(let ((properties (org-publish-cache-get filename))) |
|
1322 |
(cond ((null properties) |
|
1323 |
(unless no-create |
|
1324 |
(org-publish-cache-set filename (list property default))) |
|
1325 |
default) |
|
1326 |
((plist-member properties property) (plist-get properties property)) |
|
1327 |
(t default)))) |
|
1328 |
|
|
1329 |
(defun org-publish-cache-get (key) |
|
1330 |
"Return the value stored in `org-publish-cache' for key KEY. |
|
1331 |
Return nil, if no value or nil is found. Raise an error if the |
|
1332 |
cache does not exist." |
|
1333 |
(unless org-publish-cache |
|
1334 |
(error "`org-publish-cache-get' called, but no cache present")) |
|
1335 |
(gethash key org-publish-cache)) |
|
1336 |
|
|
1337 |
(defun org-publish-cache-set (key value) |
|
1338 |
"Store KEY VALUE pair in `org-publish-cache'. |
|
1339 |
Returns value on success, else nil. Raise an error if the cache |
|
1340 |
does not exist." |
|
1341 |
(unless org-publish-cache |
|
1342 |
(error "`org-publish-cache-set' called, but no cache present")) |
|
1343 |
(puthash key value org-publish-cache)) |
|
1344 |
|
|
1345 |
(defun org-publish-cache-ctime-of-src (file) |
|
1346 |
"Get the ctime of FILE as an integer." |
|
1347 |
(let ((attr (file-attributes |
|
1348 |
(expand-file-name (or (file-symlink-p file) file) |
|
1349 |
(file-name-directory file))))) |
|
1350 |
(if (not attr) (error "No such file: \"%s\"" file) |
|
1351 |
(+ (lsh (car (nth 5 attr)) 16) |
|
1352 |
(cadr (nth 5 attr)))))) |
|
1353 |
|
|
1354 |
|
|
1355 |
(provide 'ox-publish) |
|
1356 |
|
|
1357 |
;; Local variables: |
|
1358 |
;; generated-autoload-file: "org-loaddefs.el" |
|
1359 |
;; End: |
|
1360 |
|
|
1361 |
;;; ox-publish.el ends here |