]> code.delx.au - gnu-emacs-elpa/blob - packages/muse/muse-project.el
Merge commit '0cda39255827f283e7578cd469ae42daad9556a2' from js2-mode
[gnu-emacs-elpa] / packages / muse / muse-project.el
1 ;;; muse-project.el --- handle Muse projects
2
3 ;; Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2014
4 ;; Free Software Foundation, Inc.
5
6 ;; This file is part of Emacs Muse. It is not part of GNU Emacs.
7
8 ;; Emacs Muse is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published
10 ;; by the Free Software Foundation; either version 3, or (at your
11 ;; option) any later version.
12
13 ;; Emacs Muse is distributed in the hope that it will be useful, but
14 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 ;; General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with Emacs Muse; see the file COPYING. If not, write to the
20 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 ;; Boston, MA 02110-1301, USA.
22
23 ;;; Commentary:
24
25 ;;; Contributors:
26
27 ;;; Code:
28
29 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
30 ;;
31 ;; Muse Project Maintainance
32 ;;
33 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
34
35 (provide 'muse-project)
36
37 (require 'muse)
38 (require 'muse-publish)
39 (require 'cus-edit)
40
41 (defgroup muse-project nil
42 "Options controlling the behavior of Muse project handling."
43 :group 'muse)
44
45 (defcustom muse-before-project-publish-hook nil
46 "A hook run before a project is published.
47 Each function is passed the project object, a cons with the format
48 (PROJNAME . SETTINGS)"
49 :type 'hook
50 :group 'muse-project)
51
52 (defcustom muse-after-project-publish-hook nil
53 "A hook run after a project is published.
54 Each function is passed the project object, a cons with the format
55 (PROJNAME . SETTINGS)"
56 :type 'hook
57 :group 'muse-project)
58
59 (defvar muse-project-alist-using-customize nil
60 "Used internally by Muse to indicate whether `muse-project-alist'
61 has been modified via the customize interface.")
62 (make-variable-buffer-local 'muse-project-alist-using-customize)
63
64 (defmacro with-muse-project (project &rest body)
65 `(progn
66 (unless (muse-project ,project)
67 (error "Can't find project %s" ,project))
68 (with-temp-buffer
69 (muse-mode)
70 (setq muse-current-project (muse-project ,project))
71 (muse-project-set-variables)
72 ,@body)))
73
74 (put 'with-muse-project 'lisp-indent-function 0)
75 (put 'with-muse-project 'edebug-form-spec '(sexp body))
76
77 (defun muse-project-alist-get (sym)
78 "Turn `muse-project-alist' into something we can customize easily."
79 (when (boundp sym)
80 (setq muse-project-alist-using-customize t)
81 (let* ((val (copy-alist (symbol-value sym)))
82 (head val))
83 (while val
84 (let ((head (car (cdar val)))
85 res)
86 ;; Turn settings of first part into cons cells, symbol->string
87 (while head
88 (cond ((stringp (car head))
89 (unless (member (car head) res)
90 (setq res (append res (list (car head)))))
91 (setq head (cdr head)))
92 ((symbolp (car head))
93 (let ((x (list (symbol-name (car head)) (cadr head))))
94 (unless (member x res)
95 (setq res (append res (list x)))))
96 (setq head (cddr head)))
97 (t
98 (setq head (cdr head)))))
99 (setcdr (car val) (cons res (cdr (cdar val)))))
100 (let ((styles (cdar val)))
101 ;; Symbol->string in every style
102 (while (cdr styles)
103 (let ((head (cadr styles))
104 res)
105 (while (consp head)
106 (setq res (plist-put res (symbol-name (car head))
107 (cadr head)))
108 (setq head (cddr head)))
109 (setcdr styles (cons res (cddr styles))))
110 (setq styles (cdr styles))))
111 (setq val (cdr val)))
112 head)))
113
114 (defun muse-project-alist-set (sym val)
115 "Turn customized version of `muse-project-alist' into something
116 Muse can make use of."
117 (set sym val)
118 (when muse-project-alist-using-customize
119 ;; Make sure the unescaped version is written to .emacs
120 (put sym 'saved-value (list (custom-quote val)))
121 ;; Perform unescaping
122 (while val
123 (let ((head (car (cdar val)))
124 res)
125 ;; Turn cons cells into flat list, string->symbol
126 (while head
127 (cond ((stringp (car head))
128 (unless (member (car head) res)
129 (setq res (append res (list (car head))))))
130 ((consp (car head))
131 (let ((x (intern (caar head)))
132 (y (car (cdar head))))
133 (unless (member x res) (setq res (append res (list x))))
134 (unless (member y res) (setq res (append res (list y)))))))
135 (setq head (cdr head)))
136 (setcdr (car val) (cons res (cdr (cdar val)))))
137 (let ((styles (cdar val)))
138 ;; String->symbol in every style
139 (while (cdr styles)
140 (let ((head (cadr styles))
141 res)
142 (while (consp head)
143 (setq res (plist-put res (intern (car head))
144 (cadr head)))
145 (setq head (cddr head)))
146 (setcdr styles (cons res (cddr styles))))
147 (setq styles (cdr styles))))
148 (setq val (cdr val)))))
149
150 (define-widget 'muse-project 'default
151 "A widget that defines a Muse project."
152 :format "\n%v"
153 :value-create 'muse-widget-type-value-create
154 :value-get 'muse-widget-child-value-get
155 :value-delete 'ignore
156 :match 'muse-widget-type-match
157 :type '(cons :format " %v"
158 (repeat :tag "Settings" :format "%{%t%}:\n%v%i\n\n"
159 (choice
160 (string :tag "Directory")
161 (list :tag "Book function"
162 (const :tag ":book-funcall" ":book-funcall")
163 (choice (function)
164 (sexp :tag "Unknown")))
165 (list :tag "Book part"
166 (const :tag ":book-part" ":book-part")
167 (string :tag "Name"))
168 (list :tag "Book style"
169 (const :tag ":book-style" ":book-style")
170 (string :tag "Style"))
171 (list :tag "Default file"
172 (const :tag ":default" ":default")
173 (string :tag "File"))
174 (list :tag "End of book"
175 (const :tag ":book-end" ":book-end")
176 (const t))
177 (list :tag "Force publishing"
178 (const :tag ":force-publish" ":force-publish")
179 (repeat (string :tag "File")))
180 (list :tag "Major mode"
181 (const :tag ":major-mode" ":major-mode")
182 (choice (function :tag "Mode")
183 (sexp :tag "Unknown")))
184 (list :tag "New chapter"
185 (const :tag ":book-chapter" ":book-chapter")
186 (string :tag "Name"))
187 (list :tag "No chapters"
188 (const :tag ":nochapters" ":nochapters")
189 (const t))
190 (list :tag "Project-level publishing function"
191 (const :tag ":publish-project"
192 ":publish-project")
193 (choice (function :tag "Function")
194 (sexp :tag "Unknown")))
195 (list :tag "Set variables"
196 (const :tag ":set" ":set")
197 (repeat (list :inline t
198 (symbol :tag "Variable")
199 (sexp :tag "Setting"))))
200 (list :tag "Visit links using"
201 (const :tag ":visit-link" ":visit-link")
202 (choice (function)
203 (sexp :tag "Unknown")))))
204 (repeat :tag "Output styles" :format "%{%t%}:\n%v%i\n\n"
205 (set :tag "Style"
206 (list :inline t
207 :tag "Publishing style"
208 (const :tag ":base" ":base")
209 (string :tag "Style"))
210 (list :inline t
211 :tag "Base URL"
212 (const :tag ":base-url" ":base-url")
213 (string :tag "URL"))
214 (list :inline t
215 :tag "Exclude matching"
216 (const :tag ":exclude" ":exclude")
217 (regexp))
218 (list :inline t
219 :tag "Include matching"
220 (const :tag ":include" ":include")
221 (regexp))
222 (list :inline t
223 :tag "Timestamps file"
224 (const :tag ":timestamps" ":timestamps")
225 (file))
226 (list :inline t
227 :tag "Path"
228 (const :tag ":path" ":path")
229 (string :tag "Path"))))))
230
231 (defcustom muse-project-alist nil
232 "An alist of Muse projects.
233 A project defines a fileset, and a list of custom attributes for use
234 when publishing files in that project."
235 :type '(choice (const :tag "No projects defined." nil)
236 (repeat (cons :format "%{%t%}:\n\n%v"
237 :tag "Project" :indent 4
238 (string :tag "Project name")
239 muse-project))
240 (sexp :tag "Cannot parse expression"))
241 :get 'muse-project-alist-get
242 :set 'muse-project-alist-set
243 :group 'muse-project)
244
245 ;; Make it easier to specify a muse-project-alist entry
246
247 (defcustom muse-project-ignore-regexp
248 (concat "\\`\\(#.*#\\|.*,v\\|.*~\\|\\.\\.?\\|\\.#.*\\|,.*\\)\\'\\|"
249 "/\\(CVS\\|RCS\\|\\.arch-ids\\|{arch}\\|,.*\\|\\.svn\\|"
250 "\\.hg\\|\\.git\\|\\.bzr\\|_darcs\\)\\(/\\|\\'\\)")
251 "A regexp matching files to be ignored in Muse directories.
252
253 You should set `case-fold-search' to nil before using this regexp
254 in code."
255 :type 'regexp
256 :group 'muse-regexp)
257
258 (defcustom muse-project-publish-private-files t
259 "If this is non-nil, files will be published even if their permissions
260 are set so that no one else on the filesystem can read them.
261
262 Set this to nil if you would like to indicate that some files
263 should not be published by manually doing \"chmod o-rwx\" on
264 them.
265
266 This setting has no effect under Windows (that is, all files are
267 published regardless of permissions) because Windows lacks the
268 needed filesystem attributes."
269 :type 'boolean
270 :group 'muse-project)
271
272 (defun muse-project-recurse-directory (base)
273 "Recusively retrieve all of the directories underneath BASE.
274 A list of these directories is returned.
275
276 Directories starting with \".\" will be ignored, as well as those
277 which match `muse-project-ignore-regexp'."
278 (let ((case-fold-search nil)
279 list dir)
280 (when (and (file-directory-p base)
281 (not (string-match muse-project-ignore-regexp base)))
282 (dolist (file (directory-files base t "^[^.]"))
283 (when (and (file-directory-p file)
284 (not (string-match muse-project-ignore-regexp file)))
285 (setq dir (file-name-nondirectory file))
286 (push dir list)
287 (nconc list (mapcar #'(lambda (item)
288 (concat dir "/" item))
289 (muse-project-recurse-directory file)))))
290 list)))
291
292 (defun muse-project-alist-styles (entry-dir output-dir style &rest other)
293 "Return a list of styles to use in a `muse-project-alist' entry.
294 ENTRY-DIR is the top-level directory of the project.
295 OUTPUT-DIR is where Muse files are published, keeping directory structure.
296 STYLE is the publishing style to use.
297
298 OTHER contains other definitions to add to each style. It is optional.
299
300 For an example of the use of this function, see
301 `examples/mwolson/muse-init.el' from the Muse distribution."
302 (let ((fnd (file-name-nondirectory entry-dir)))
303 (when (string= fnd "")
304 ;; deal with cases like "foo/" that have a trailing slash
305 (setq fnd (file-name-nondirectory (substring entry-dir 0 -1))))
306 (cons `(:base ,style :path ,(if (muse-file-remote-p output-dir)
307 output-dir
308 (expand-file-name output-dir))
309 :include ,(concat "/" fnd "/[^/]+$")
310 ,@other)
311 (mapcar (lambda (dir)
312 `(:base ,style
313 :path ,(expand-file-name dir output-dir)
314 :include ,(concat "/" dir "/[^/]+$")
315 ,@other))
316 (muse-project-recurse-directory entry-dir)))))
317
318 (defun muse-project-alist-dirs (entry-dir)
319 "Return a list of directories to use in a `muse-project-alist' entry.
320 ENTRY-DIR is the top-level directory of the project.
321
322 For an example of the use of this function, see
323 `examples/mwolson/muse-init.el' from the Muse distribution."
324 (cons (expand-file-name entry-dir)
325 (mapcar (lambda (dir) (expand-file-name dir entry-dir))
326 (muse-project-recurse-directory entry-dir))))
327
328 ;; Constructing the file-alist
329
330 (defvar muse-project-file-alist nil
331 "This variable is automagically constructed as needed.")
332
333 (defvar muse-project-file-alist-hook nil
334 "Functions that are to be exectuted immediately after updating
335 `muse-project-file-alist'.")
336
337 (defvar muse-current-project nil
338 "Project we are currently visiting.")
339 (make-variable-buffer-local 'muse-current-project)
340 (defvar muse-current-project-global nil
341 "Project we are currently visiting. This is used to propagate the value
342 of `muse-current-project' into a new buffer during publishing.")
343
344 (defvar muse-current-output-style nil
345 "The output style that we are currently using for publishing files.")
346
347 (defsubst muse-project (&optional project)
348 "Resolve the given PROJECT into a full Muse project, if it is a string."
349 (if (null project)
350 (or muse-current-project
351 (muse-project-of-file))
352 (if (stringp project)
353 (assoc project muse-project-alist)
354 (muse-assert (consp project))
355 project)))
356
357 (defun muse-project-page-file (page project &optional no-check-p)
358 "Return a filename if PAGE exists within the given Muse PROJECT."
359 (setq project (muse-project project))
360 (if (null page)
361 ;; if not given a page, return the first directory instead
362 (let ((pats (cadr project)))
363 (catch 'done
364 (while pats
365 (if (symbolp (car pats))
366 (setq pats (cddr pats))
367 (throw 'done (file-name-as-directory (car pats)))))))
368 (let ((dir (file-name-directory page))
369 (expanded-path nil))
370 (when dir
371 (setq expanded-path (concat (expand-file-name
372 page
373 (file-name-directory (muse-current-file)))
374 (when muse-file-extension
375 (concat "." muse-file-extension))))
376 (setq page (file-name-nondirectory page)))
377 (let ((files (muse-collect-alist
378 (muse-project-file-alist project no-check-p)
379 page))
380 (matches nil))
381 (if dir
382 (catch 'done
383 (save-match-data
384 (dolist (file files)
385 (if (and expanded-path
386 (string= expanded-path (cdr file)))
387 (throw 'done (cdr file))
388 (let ((pos (string-match (concat (regexp-quote dir) "\\'")
389 (file-name-directory
390 (cdr file)))))
391 (when pos
392 (setq matches (cons (cons pos (cdr file))
393 matches)))))))
394 ;; if we haven't found an exact match, pick a candidate
395 (car (muse-sort-by-rating matches)))
396 (dolist (file files)
397 (setq matches (cons (cons (length (cdr file)) (cdr file))
398 matches)))
399 (car (muse-sort-by-rating matches '<)))))))
400
401 (defun muse-project-private-p (file)
402 "Return non-nil if NAME is a private page with PROJECT."
403 (unless (or muse-under-windows-p
404 muse-project-publish-private-files)
405 (setq file (file-truename file))
406 (if (file-attributes file) ; don't publish if no attributes exist
407 (or (when (eq ?- (aref (nth 8 (file-attributes
408 (file-name-directory file))) 7))
409 (message (concat
410 "The " (file-name-directory file)
411 " directory must be readable by others"
412 " in order for its contents to be published.")))
413 (eq ?- (aref (nth 8 (file-attributes file)) 7)))
414 t)))
415
416 (defun muse-project-file-entries (path)
417 (let* ((names (list t))
418 (lnames names)
419 (case-fold-search nil))
420 (cond
421 ((file-directory-p path)
422 (dolist (file (directory-files
423 path t (when (and muse-file-extension
424 (not (string= muse-file-extension "")))
425 (concat "." muse-file-extension "\\'"))))
426 (unless (or (string-match muse-project-ignore-regexp file)
427 (string-match muse-project-ignore-regexp
428 (file-name-nondirectory file))
429 (file-directory-p file))
430 (setcdr lnames
431 (cons (cons (muse-page-name file) file) nil))
432 (setq lnames (cdr lnames)))))
433 ((file-readable-p path)
434 (setcdr lnames
435 (cons (cons (muse-page-name path) path) nil))
436 (setq lnames (cdr lnames)))
437 (t ; regexp
438 (muse-assert (file-name-directory path))
439 (dolist (file (directory-files
440 (file-name-directory path) t
441 (file-name-nondirectory path)))
442 (unless (or (string-match muse-project-ignore-regexp file)
443 (string-match muse-project-ignore-regexp
444 (file-name-nondirectory file)))
445 (setcdr lnames
446 (cons (cons (muse-page-name file) file) nil))
447 (setq lnames (cdr lnames))))))
448 (cdr names)))
449
450 (defvar muse-updating-file-alist-p nil
451 "Make sure that recursive calls to `muse-project-file-alist' are bounded.")
452
453 (defun muse-project-determine-last-mod (project &optional no-check-p)
454 "Return the most recent last-modified timestamp of dirs in PROJECT."
455 (let ((last-mod nil))
456 (unless (or muse-under-windows-p no-check-p)
457 (let ((pats (cadr project)))
458 (while pats
459 (if (symbolp (car pats))
460 (setq pats (cddr pats))
461 (let* ((fnd (file-name-directory (car pats)))
462 (dir (cond ((file-directory-p (car pats))
463 (car pats))
464 ((and (not (file-readable-p (car pats)))
465 fnd
466 (file-directory-p fnd))
467 fnd))))
468 (when dir
469 (let ((mod-time (nth 5 (file-attributes dir))))
470 (when (or (null last-mod)
471 (and mod-time
472 (muse-time-less-p last-mod mod-time)))
473 (setq last-mod mod-time)))))
474 (setq pats (cdr pats))))))
475 last-mod))
476
477 (defun muse-project-file-alist (&optional project no-check-p)
478 "Return member filenames for the given Muse PROJECT.
479 Also, update the `muse-project-file-alist' variable.
480
481 On UNIX, this alist is only updated if one of the directories'
482 contents have changed. On Windows, it is always reread from
483 disk.
484
485 If NO-CHECK-P is non-nil, do not update the alist, just return
486 the current one."
487 (setq project (muse-project project))
488 (when (and project muse-project-alist)
489 (let* ((file-alist (assoc (car project) muse-project-file-alist))
490 (last-mod (muse-project-determine-last-mod project no-check-p)))
491 ;; Either return the currently known list, or read it again from
492 ;; disk
493 (if (or (and no-check-p (cadr file-alist))
494 muse-updating-file-alist-p
495 (not (or muse-under-windows-p
496 (null (cddr file-alist))
497 (null last-mod)
498 (muse-time-less-p (cddr file-alist) last-mod))))
499 (cadr file-alist)
500 (if file-alist
501 (setcdr (cdr file-alist) last-mod)
502 (setq file-alist (cons (car project) (cons nil last-mod))
503 muse-project-file-alist
504 (cons file-alist muse-project-file-alist)))
505 ;; Read in all of the file entries
506 (let ((muse-updating-file-alist-p t))
507 (prog1
508 (save-match-data
509 (setcar
510 (cdr file-alist)
511 (let* ((names (list t))
512 (pats (cadr project)))
513 (while pats
514 (if (symbolp (car pats))
515 (setq pats (cddr pats))
516 (nconc names (muse-project-file-entries (car pats)))
517 (setq pats (cdr pats))))
518 (cdr names))))
519 (run-hooks 'muse-project-file-alist-hook)))))))
520
521 (defun muse-project-add-to-alist (file &optional project)
522 "Make sure FILE is added to `muse-project-file-alist'.
523
524 It works by either calling the `muse-project-file-alist' function
525 if a directory has been modified since we last checked, or
526 manually forcing the file entry to exist in the alist. This
527 works around an issue where if several files being saved at the
528 same time, only the first one will make it into the alist. It is
529 meant to be called by `muse-project-after-save-hook'.
530
531 The project of the file is determined by either the PROJECT
532 argument, or `muse-project-of-file' if PROJECT is not specified."
533 (setq project (or (muse-project project) (muse-project-of-file file)))
534 (when (and project muse-project-alist)
535 (let* ((file-alist (assoc (car project) muse-project-file-alist))
536 (last-mod (muse-project-determine-last-mod project)))
537 ;; Determine whether we need to call this
538 (if (or (null (cddr file-alist))
539 (null last-mod)
540 (muse-time-less-p (cddr file-alist) last-mod))
541 ;; The directory will show up as modified, so go ahead and
542 ;; call `muse-project-file-alist'
543 (muse-project-file-alist project)
544 ;; It is not showing as modified, so forcefully add the
545 ;; current file to the project file-alist
546 (let ((muse-updating-file-alist-p t))
547 (prog1
548 (save-match-data
549 (setcar (cdr file-alist)
550 (nconc (muse-project-file-entries file)
551 (cadr file-alist))))
552 (run-hooks 'muse-project-file-alist-hook)))))))
553
554 (defun muse-project-of-file (&optional pathname)
555 "Determine which project the given PATHNAME relates to.
556 If PATHNAME is nil, the current buffer's filename is used."
557 (if (and (null pathname) muse-current-project)
558 muse-current-project
559 (unless pathname (setq pathname (muse-current-file)))
560 (save-match-data
561 (when (and (stringp pathname)
562 muse-project-alist
563 (not (string= pathname ""))
564 (not (let ((case-fold-search nil))
565 (or (string-match muse-project-ignore-regexp
566 pathname)
567 (string-match muse-project-ignore-regexp
568 (file-name-nondirectory
569 pathname))))))
570 (let* ((file (file-truename pathname))
571 (dir (file-name-directory file))
572 found rating matches)
573 (catch 'found
574 (dolist (project-entry muse-project-alist)
575 (let ((pats (cadr project-entry)))
576 (while pats
577 (if (symbolp (car pats))
578 (setq pats (cddr pats))
579 (let ((tname (file-truename (car pats))))
580 (cond ((or (string= tname file)
581 (string= (file-name-as-directory tname) dir))
582 (throw 'found project-entry))
583 ((string-match (concat "\\`" (regexp-quote tname))
584 file)
585 (setq matches (cons (cons (match-end 0)
586 project-entry)
587 matches)))))
588 (setq pats (cdr pats))))))
589 ;; if we haven't found an exact match, pick a candidate
590 (car (muse-sort-by-rating matches))))))))
591
592 (defun muse-project-after-save-hook ()
593 "Update Muse's file-alist if we are saving a Muse file."
594 (let ((project (muse-project-of-file)))
595 (when project
596 (muse-project-add-to-alist (buffer-file-name) project))))
597
598 (add-hook 'after-save-hook 'muse-project-after-save-hook)
599
600 (defun muse-read-project (prompt &optional no-check-p no-assume)
601 "Read a project name from the minibuffer, if it can't be figured
602 out."
603 (if (null muse-project-alist)
604 (error "There are no Muse projects defined; see `muse-project-alist'")
605 (or (unless no-check-p
606 (muse-project-of-file))
607 (if (and (not no-assume)
608 (= 1 (length muse-project-alist)))
609 (car muse-project-alist)
610 (assoc (funcall muse-completing-read-function
611 prompt muse-project-alist)
612 muse-project-alist)))))
613
614 (defvar muse-project-page-history nil)
615
616 (defun muse-read-project-file (project prompt &optional default)
617 (let* ((file-list (muse-delete-dups
618 (mapcar #'(lambda (a) (list (car a)))
619 (muse-project-file-alist project))))
620 (name (funcall muse-completing-read-function
621 prompt file-list nil nil nil
622 'muse-project-page-history default)))
623 (cons name (muse-project-page-file name project))))
624
625 ;;;###autoload
626 (defun muse-project-find-file (name project &optional command directory)
627 "Open the Muse page given by NAME in PROJECT.
628 If COMMAND is non-nil, it is the function used to visit the file.
629 If DIRECTORY is non-nil, it is the directory in which the page
630 will be created if it does not already exist. Otherwise, the
631 first directory within the project's fileset is used."
632 (interactive
633 (let* ((project (muse-read-project "Find in project: "
634 current-prefix-arg))
635 (default (muse-get-keyword :default (cadr project)))
636 (entry (muse-read-project-file
637 project (if default
638 (format "Find page: (default: %s) "
639 default)
640 "Find page: ")
641 default)))
642 (list entry project)))
643 (setq project (muse-project project))
644 (let ((project-name (car project)))
645 (unless (interactive-p)
646 (setq project (muse-project project)
647 name (cons name (muse-project-page-file name project))))
648 ;; If we're given a relative or absolute filename, open it as-is
649 (if (and (car name)
650 (save-match-data
651 (or (string-match "\\`\\.+/" (car name))
652 (string-match muse-file-regexp (car name))
653 (string-match muse-image-regexp (car name)))))
654 (setcdr name (car name))
655 ;; At this point, name is (PAGE . FILE).
656 (unless (cdr name)
657 (let ((pats (cadr project)))
658 (while (and pats (null directory))
659 (if (symbolp (car pats))
660 (setq pats (cddr pats))
661 (if (file-directory-p (car pats))
662 (setq directory (car pats) pats nil)
663 (setq pats (cdr pats))))))
664 (when directory
665 (let ((filename (expand-file-name (car name) directory)))
666 (when (and muse-file-extension
667 (not (string= muse-file-extension ""))
668 (not (file-exists-p (car name))))
669 (setq filename (concat filename "." muse-file-extension)))
670 (unless (file-exists-p directory)
671 (make-directory directory t))
672 (setcdr name filename)))))
673 ;; Open the file
674 (if (cdr name)
675 (funcall (or command 'find-file) (cdr name))
676 (error "There is no page %s in project %s"
677 (car name) project-name))))
678
679 (defun muse-project-choose-style (closure test styles)
680 "Run TEST on STYLES and return first style where TEST yields non-nil.
681 TEST should take two arguments. The first is CLOSURE, which is
682 passed verbatim. The second if the current style to consider.
683
684 If no style passes TEST, return the first style."
685 (or (catch 'winner
686 (dolist (style styles)
687 (when (funcall test closure style)
688 (throw 'winner style))))
689 (car styles)))
690
691 (defun muse-project-choose-style-by-link-suffix (given-suffix style)
692 "If the given STYLE has a link-suffix that equals GIVEN-SUFFIX,
693 return non-nil."
694 (let ((link-suffix (or (muse-style-element :link-suffix style)
695 (muse-style-element :suffix style))))
696 (and (stringp link-suffix)
697 (string= given-suffix link-suffix))))
698
699 (defun muse-project-applicable-styles (file styles)
700 "Given STYLES, return a list of the ones that are considered for FILE.
701 The name of a project may be used for STYLES."
702 (when (stringp styles)
703 (setq styles (cddr (muse-project styles))))
704 (when (and file styles)
705 (let ((used-styles nil))
706 (dolist (style styles)
707 (let ((include-regexp (muse-style-element :include style))
708 (exclude-regexp (muse-style-element :exclude style))
709 (rating nil))
710 (when (and (or (and (null include-regexp)
711 (null exclude-regexp))
712 (if include-regexp
713 (setq rating (string-match include-regexp file))
714 (not (string-match exclude-regexp file))))
715 (file-exists-p file)
716 (not (muse-project-private-p file)))
717 (setq used-styles (cons (cons rating style) used-styles)))))
718 (muse-sort-by-rating (nreverse used-styles)))))
719
720 (defun muse-project-get-applicable-style (file styles)
721 "Choose a style from the STYLES that FILE can publish to.
722 The user is prompted if several styles are found."
723 (muse-publish-get-style
724 (mapcar (lambda (style)
725 (cons (muse-get-keyword :base style) style))
726 (muse-project-applicable-styles file styles))))
727
728 (defun muse-project-resolve-directory (page local-style remote-style)
729 "Figure out the directory part of the path that provides a link to PAGE.
730 LOCAL-STYLE is the style of the current Muse file, and
731 REMOTE-STYLE is the style associated with PAGE.
732
733 If REMOTE-STYLE has a :base-url element, concatenate it and PAGE.
734 Otherwise, return a relative link."
735 (let ((prefix (muse-style-element :base-url remote-style)))
736 (if prefix
737 (concat prefix page)
738 (file-relative-name (expand-file-name
739 (file-name-nondirectory page)
740 (muse-style-element :path remote-style))
741 (expand-file-name
742 (muse-style-element :path local-style))))))
743
744 (defun muse-project-resolve-link (page local-style remote-styles)
745 "Return a published link from the output path of one file to another file.
746
747 The best match for PAGE is determined by comparing the link
748 suffix of the given local style and that of the remote styles.
749
750 The remote styles are usually populated by
751 `muse-project-applicable-styles'.
752
753 If no remote style is found, return PAGE verbatim
754
755 If PAGE has a :base-url associated with it, return the
756 concatenation of the :base-url value and PAGE.
757
758 Otherwise, return a relative path from the directory of
759 LOCAL-STYLE to the best directory among REMOTE-STYLES."
760 (let ((link-suffix (or (muse-style-element :link-suffix local-style)
761 (muse-style-element :suffix local-style)))
762 remote-style)
763 (if (not (stringp link-suffix))
764 (setq remote-style (car remote-styles))
765 (setq remote-style (muse-project-choose-style
766 link-suffix
767 #'muse-project-choose-style-by-link-suffix
768 remote-styles)))
769 (if (null remote-style)
770 page
771 (setq page (muse-project-resolve-directory
772 page local-style remote-style))
773 (concat (file-name-directory page)
774 (muse-publish-link-name page remote-style)))))
775
776 (defun muse-project-current-output-style (&optional file project)
777 (or muse-current-output-style
778 (progn
779 (unless file (setq file (muse-current-file)))
780 (unless project (setq project (muse-project-of-file file)))
781 (car (muse-project-applicable-styles file (cddr project))))))
782
783 (defun muse-project-link-page (page)
784 (let ((project (muse-project-of-file)))
785 (muse-project-resolve-link page
786 (muse-project-current-output-style)
787 (muse-project-applicable-styles
788 (muse-project-page-file page project)
789 (cddr project)))))
790
791 (defun muse-project-publish-file-default (file style output-dir force)
792 ;; ensure the publishing location is available
793 (unless (file-exists-p output-dir)
794 (message "Creating publishing directory %s" output-dir)
795 (make-directory output-dir t))
796 ;; publish the member file!
797 (muse-publish-file file style output-dir force))
798
799 (defun muse-project-publish-file (file styles &optional force)
800 (setq styles (muse-project-applicable-styles file styles))
801 (let (published)
802 (dolist (style styles)
803 (if (or (not (listp style))
804 (not (cdr style)))
805 (muse-display-warning
806 (concat "Skipping malformed muse-project-alist style."
807 "\nPlease double-check your configuration,"))
808 (let ((output-dir (muse-style-element :path style))
809 (muse-current-output-style style)
810 (fun (or (muse-style-element :publish style t)
811 'muse-project-publish-file-default)))
812 (when (funcall fun file style output-dir force)
813 (setq published t)))))
814 published))
815
816 ;;;###autoload
817 (defun muse-project-publish-this-file (&optional force style)
818 "Publish the currently-visited file according to `muse-project-alist',
819 prompting if more than one style applies.
820
821 If FORCE is given, publish the file even if it is up-to-date.
822
823 If STYLE is given, use that publishing style rather than
824 prompting for one."
825 (interactive (list current-prefix-arg))
826 (let ((muse-current-project (muse-project-of-file)))
827 (if (not muse-current-project)
828 ;; file is not part of a project, so fall back to muse-publish
829 (if (interactive-p) (call-interactively 'muse-publish-this-file)
830 (muse-publish-this-file style nil force))
831 (unless style
832 (setq style (muse-project-get-applicable-style
833 buffer-file-name (cddr muse-current-project))))
834 (let* ((output-dir (muse-style-element :path style))
835 (muse-current-project-global muse-current-project)
836 (muse-current-output-style (list :base (car style)
837 :path output-dir))
838 (fun (or (muse-style-element :publish style t)
839 'muse-project-publish-file-default)))
840 (unless (funcall fun buffer-file-name style output-dir force)
841 (message (concat "The published version is up-to-date; use"
842 " C-u C-c C-t to force an update.")))))))
843
844 (defun muse-project-save-buffers (&optional project)
845 (setq project (muse-project project))
846 (when project
847 (save-excursion
848 (map-y-or-n-p
849 (function
850 (lambda (buffer)
851 (and (buffer-modified-p buffer)
852 (not (buffer-base-buffer buffer))
853 (or (buffer-file-name buffer)
854 (progn
855 (set-buffer buffer)
856 (and buffer-offer-save
857 (> (buffer-size) 0))))
858 (with-current-buffer buffer
859 (let ((proj (muse-project-of-file)))
860 (and proj (string= (car proj)
861 (car project)))))
862 (if (buffer-file-name buffer)
863 (format "Save file %s? "
864 (buffer-file-name buffer))
865 (format "Save buffer %s? "
866 (buffer-name buffer))))))
867 (function
868 (lambda (buffer)
869 (set-buffer buffer)
870 (save-buffer)))
871 (buffer-list)
872 '("buffer" "buffers" "save")
873 (if (boundp 'save-some-buffers-action-alist)
874 save-some-buffers-action-alist)))))
875
876 (defun muse-project-publish-default (project styles &optional force)
877 "Publish the pages of PROJECT that need publishing."
878 (setq project (muse-project project))
879 (let ((published nil))
880 ;; publish all files in the project, for each style; the actual
881 ;; publishing will only happen if the files are newer than the
882 ;; last published output, or if the file is listed in
883 ;; :force-publish. Files in :force-publish will not trigger the
884 ;; "All pages need to be published" message.
885 (let ((forced-files (muse-get-keyword :force-publish (cadr project)))
886 (file-alist (muse-project-file-alist project)))
887 (dolist (pair file-alist)
888 (when (muse-project-publish-file (cdr pair) styles force)
889 (setq forced-files (delete (car pair) forced-files))
890 (setq published t)))
891 (dolist (file forced-files)
892 (muse-project-publish-file (cdr (assoc file file-alist)) styles t)))
893 ;; run hook after publishing ends
894 (run-hook-with-args 'muse-after-project-publish-hook project)
895 ;; notify the user that everything is now done
896 (if published
897 (message "All pages in %s have been published." (car project))
898 (message "No pages in %s need publishing at this time."
899 (car project)))))
900
901 ;;;###autoload
902 (defun muse-project-publish (project &optional force)
903 "Publish the pages of PROJECT that need publishing."
904 (interactive (list (muse-read-project "Publish project: " nil t)
905 current-prefix-arg))
906 (setq project (muse-project project))
907 (let ((styles (cddr project))
908 (muse-current-project project)
909 (muse-current-project-global project))
910 ;; determine the style from the project, or else ask
911 (unless styles
912 (setq styles (list (muse-publish-get-style))))
913 (unless project
914 (error "Cannot find a project to publish"))
915 ;; prompt to save any buffers related to this project
916 (muse-project-save-buffers project)
917 ;; run hook before publishing begins
918 (run-hook-with-args 'muse-before-project-publish-hook project)
919 ;; run the project-level publisher
920 (let ((fun (or (muse-get-keyword :publish-project (cadr project) t)
921 'muse-project-publish-default)))
922 (funcall fun project styles force))))
923
924 (defun muse-project-batch-publish ()
925 "Publish Muse files in batch mode."
926 (let ((muse-batch-publishing-p t)
927 force)
928 (if (string= "--force" (or (car command-line-args-left) ""))
929 (setq force t
930 command-line-args-left (cdr command-line-args-left)))
931 (if command-line-args-left
932 (dolist (project command-line-args-left)
933 (message "Publishing project %s ..." project)
934 (muse-project-publish project force))
935 (message "No projects specified."))))
936
937 (eval-when-compile
938 (put 'make-local-hook 'byte-compile nil))
939
940 (defun muse-project-set-variables ()
941 "Load project-specific variables."
942 (when (and muse-current-project-global (null muse-current-project))
943 (setq muse-current-project muse-current-project-global))
944 (let ((vars (muse-get-keyword :set (cadr muse-current-project)))
945 sym custom-set var)
946 (while vars
947 (setq sym (car vars))
948 (setq custom-set (or (get sym 'custom-set) 'set))
949 (setq var (if (eq (get sym 'custom-type) 'hook)
950 (if (fboundp 'make-local-hook)
951 (make-local-hook sym))
952 (make-local-variable sym)))
953 (funcall custom-set var (car (cdr vars)))
954 (setq vars (cdr (cdr vars))))))
955
956 (custom-add-option 'muse-before-publish-hook 'muse-project-set-variables)
957 (add-to-list 'muse-before-publish-hook 'muse-project-set-variables)
958
959 (defun muse-project-delete-output-files (project)
960 (interactive
961 (list (muse-read-project "Remove all output files for project: " nil t)))
962 (setq project (muse-project project))
963 (let ((file-alist (muse-project-file-alist project))
964 (styles (cddr project))
965 output-file path)
966 (dolist (entry file-alist)
967 (dolist (style styles)
968 (setq output-file
969 (and (setq path (muse-style-element :path style))
970 (expand-file-name
971 (concat (muse-style-element :prefix style)
972 (car entry)
973 (or (muse-style-element :osuffix style)
974 (muse-style-element :suffix style)))
975 path)))
976 (if output-file
977 (muse-delete-file-if-exists output-file))))))
978
979 ;;; muse-project.el ends here