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