Provide optional outline-style faces; customization fixes; experimental stuff.
[muse-el.git] / muse-mode.el
blobcfc498e4c2597ab66dc824a5e4442c49a06d5ba7
1 ;;; muse-mode.el --- Mode for editing Muse files; has font-lock support.
3 ;; Copyright (C) 2004, 2005 Free Software Foundation, Inc.
5 ;; This file is not part of GNU Emacs.
7 ;; This is free software; you can redistribute it and/or modify it under
8 ;; the terms of the GNU General Public License as published by the Free
9 ;; Software Foundation; either version 2, or (at your option) any later
10 ;; version.
12 ;; This is distributed in the hope that it will be useful, but WITHOUT
13 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 ;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 ;; for more details.
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with GNU Emacs; see the file COPYING. If not, write to the
19 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 ;; Boston, MA 02110-1301, USA.
22 ;;; Commentary:
24 ;; The Emacs Muse major mode is basically a hyped-up text-mode which
25 ;; knows a lot more about the apparent structure of the document.
27 ;;; Contributors:
29 ;; Andrea Riciputi (ariciputi AT pito DOT com) gave an initial
30 ;; implementation for tag completion by means of the
31 ;; `muse-insert-tag' function.
33 ;;; Code:
35 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
37 ;; Emacs Muse Major Mode
39 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
41 (require 'muse)
42 (require 'muse-regexps)
43 (require 'muse-project)
44 (require 'muse-publish)
46 (autoload 'muse-use-font-lock "muse-colors")
48 (require 'derived)
49 (eval-when-compile
50 (condition-case nil
51 (require 'pcomplete) ; load if available
52 (error nil)))
54 ;;; Options:
56 (defgroup muse-mode nil
57 "Options controlling the behavior of the Muse editing Mode.
58 See `muse-publish' for more information."
59 :group 'muse)
61 (defcustom muse-mode-highlight-p t
62 "If non-nil, highlight the content of Muse buffers."
63 :type 'boolean
64 :require 'muse-colors
65 :group 'muse-mode)
67 (defcustom muse-mode-auto-p t
68 "If non-nil, automagically determine when Muse mode should be activated."
69 :type 'boolean
70 :set (function
71 (lambda (sym value)
72 (if value
73 (add-hook 'find-file-hooks 'muse-mode-maybe)
74 (remove-hook 'find-file-hooks 'muse-mode-maybe))
75 (set sym value)))
76 :group 'muse-mode)
78 (defcustom muse-mode-hook nil
79 "A hook that is run when Muse mode is entered."
80 :type 'hook
81 :options '(flyspell-mode footnote-mode turn-on-auto-fill
82 highlight-changes-mode)
83 :group 'muse-mode)
85 (defvar muse-mode-map
86 (let ((map (make-sparse-keymap)))
87 (define-key map [(control ?c) (control ?a)] 'muse-index)
88 (define-key map [(control ?c) (control ?b)] 'muse-browse-result)
89 (define-key map [(control ?c) (control ?c)] 'muse-follow-name-at-point)
90 (define-key map [(control ?c) (control ?e)] 'muse-edit-link-at-point)
91 (define-key map [(control ?c) (control ?t)] 'muse-publish-this-file)
92 (define-key map [(control ?c) (control ?v)] 'muse-follow-name-at-point)
94 (define-key map [(control ?c) (control ?l)] 'font-lock-mode)
96 (define-key map [(control ?c) ?=] 'muse-what-changed)
98 (define-key map [tab] 'muse-next-reference)
99 (define-key map [(control ?i)] 'muse-next-reference)
101 (if (featurep 'xemacs)
102 (progn
103 (define-key map [(button2)] 'muse-follow-name-at-mouse)
104 (define-key map [(shift button2)]
105 'muse-follow-name-at-mouse-other-window))
106 (define-key map [(shift control ?m)]
107 'muse-follow-name-at-point-other-window)
108 (define-key map [mouse-2] 'muse-follow-name-at-mouse)
109 (define-key map [(shift mouse-2)]
110 'muse-follow-name-at-mouse-other-window))
112 (if (featurep 'xemacs)
113 (define-key map [(shift tab)] 'muse-previous-reference)
114 (define-key map [(shift iso-lefttab)] 'muse-previous-reference)
115 (define-key map [(shift control ?i)] 'muse-previous-reference))
117 (define-key map [(control ?c) (control ?f)] 'muse-project-find-file)
118 (define-key map [(control ?c) (control ?p)] 'muse-project-publish)
120 (define-key map [(control ?c) tab] 'muse-insert-tag)
121 (define-key map [(control ?c) (control ?i)] 'muse-insert-tag)
123 (when (featurep 'pcomplete)
124 (define-key map [(meta tab)] 'pcomplete)
125 (define-key map [(meta control ?i)] 'pcomplete))
127 map)
128 "Keymap used by Emacs Muse mode.")
130 ;; Code:
132 ;;;###autoload
133 (define-derived-mode muse-mode text-mode "Muse"
134 "Muse is an Emacs mode for authoring and publishing documents.
135 \\{muse-mode-map}"
136 ;; because we're not inheriting from normal-mode, we need to
137 ;; explicitly run file variables if the user wants to
138 (condition-case err
139 (hack-local-variables)
140 (error (message "File local-variables error: %s"
141 (prin1-to-string err))))
142 (if muse-mode-highlight-p
143 (muse-use-font-lock))
144 (setq muse-current-project (muse-project-of-file))
145 (muse-project-set-variables)
146 (when (featurep 'pcomplete)
147 ;; if pcomplete is available, set it up!
148 (set (make-variable-buffer-local 'pcomplete-default-completion-function)
149 'muse-mode-completions)
150 (set (make-variable-buffer-local 'pcomplete-command-completion-function)
151 'muse-mode-completions)
152 (set (make-variable-buffer-local 'pcomplete-parse-arguments-function)
153 'muse-mode-current-word)))
155 (defun muse-mode-maybe ()
156 "Maybe turn Emacs Muse mode on for this file."
157 (let ((project (muse-project-of-file)))
158 (and project
159 (funcall (or (muse-get-keyword :major-mode (cadr project) t)
160 'muse-mode)))))
162 ;;; Support page name completion using pcomplete
164 (defun muse-completions ()
165 "Return a list of possible completions names for this buffer."
166 (let ((project (muse-project-of-file)))
167 (if project
168 (while (pcomplete-here
169 (mapcar 'car (muse-project-file-alist project)))))))
171 (defun muse-current-word ()
172 (let ((end (point)))
173 (save-restriction
174 (save-excursion
175 (skip-chars-backward (concat "^\\["
176 muse-regexp-space))
177 (narrow-to-region (point) end))
178 (pcomplete-parse-buffer-arguments))))
180 ;;; Navigate/visit links or URLs. Use TAB, S-TAB and RET (or mouse-2).
182 (defun muse-link-at-point (&optional pos)
183 "Return link text if a URL or Muse link name is at point."
184 (let ((case-fold-search nil)
185 (here (or pos (point))))
186 (when (or (null pos)
187 (and (char-after pos)
188 (not (eq (char-syntax (char-after pos)) ?\ ))))
189 (save-excursion
190 (goto-char here)
191 (skip-chars-backward (concat "^'\"<>{}("
192 muse-regexp-space))
193 (or (and (looking-at muse-url-regexp)
194 (match-string 0))
195 (and (or (looking-at muse-link-regexp)
196 (and (search-backward "[[" (line-beginning-position) t)
197 (looking-at muse-link-regexp)))
198 (<= here (match-end 0))
199 (match-string 1)))))))
201 (defun muse-make-link (link &optional name)
202 "Return a link to LINK with NAME as the text."
203 (if (and name
204 link
205 (not (string= name ""))
206 (not (string= link name)))
207 (concat "[[" (or link "") "][" name "]]")
208 (concat "[[" (or link "") "]]")))
210 (defun muse-edit-link-at-point ()
211 "Edit the current link.
212 Do not rename the page originally referred to."
213 (interactive)
214 (let (old-name)
215 (if (muse-link-at-point)
216 (replace-match
217 (muse-make-link
218 (read-string "Link: "
219 (match-string-no-properties 1))
220 (read-string "Text: "
221 (match-string-no-properties 2)))
222 t t)
223 (error "There is no valid link at point"))))
225 (defun muse-visit-link (link &optional other-window)
226 "Visit the URL or link named by LINK-NAME."
227 (let ((visit-link-function
228 (muse-get-keyword :visit-link (cadr (muse-project-of-file)) t)))
229 (if visit-link-function
230 (funcall visit-link-function link other-window)
231 (if (string-match muse-url-regexp link)
232 (browse-url link)
233 (let (anchor)
234 (if (string-match "#" link)
235 (setq anchor (substring link (match-beginning 0))
236 link (substring link 0 (match-beginning 0))))
237 (let ((project (muse-project-of-file)))
238 (if project
239 (muse-project-find-file link project
240 (and other-window
241 'find-file-other-window))
242 (if other-window
243 (find-file-other-window link)
244 (find-file link))))
245 (if anchor
246 (search-forward anchor nil t)))))))
248 (defun muse-browse-result (style &optional other-window)
249 "Visit the current page's published result."
250 (interactive (list (muse-publish-get-style) current-prefix-arg))
251 (setq style (muse-style style))
252 (let ((result-path
253 (muse-publish-output-file buffer-file-name
254 (muse-style-element :path style) style)))
255 (if (not (file-readable-p result-path))
256 (error "Cannot open output file '%s" result-path)
257 (if other-window
258 (find-file-other-window result-path)
259 (let ((func (muse-style-element :browser style t)))
260 (if func
261 (funcall func result-path)
262 (message "The publishing style %s does not support browsing."
263 style)))))))
265 (defun muse-follow-name-at-point (&optional other-window)
266 "Visit the link at point, or insert a newline if none."
267 (interactive "P")
268 (let ((link (muse-link-at-point)))
269 (if link
270 (muse-visit-link link other-window)
271 (error "There is no valid link at point"))))
273 (defun muse-follow-name-at-point-other-window ()
274 "Visit the link at point in other window."
275 (interactive)
276 (muse-follow-name-at-point t))
278 (defun muse-follow-name-at-mouse (event &optional other-window)
279 "Visit the link at point, or yank text if none."
280 (interactive "eN")
281 (save-excursion
282 (cond ((fboundp 'event-window) ; XEmacs
283 (set-buffer (window-buffer (event-window event)))
284 (and (funcall (symbol-function 'event-point) event)
285 (goto-char (funcall (symbol-function 'event-point) event))))
286 ((fboundp 'posn-window) ; Emacs
287 (set-buffer (window-buffer (posn-window (event-start event))))
288 (goto-char (posn-point (event-start event)))))
289 (muse-follow-name-at-point other-window)))
291 (defun muse-follow-name-at-mouse-other-window (event)
292 "Visit the link at point"
293 (interactive "e")
294 ;; throw away the old window position, since other-window will
295 ;; change it anyway
296 (select-window (car (cadr event)))
297 (muse-follow-name-at-mouse event t))
299 (defun muse-next-reference ()
300 "Move forward to next Muse link or URL, cycling if necessary."
301 (interactive)
302 (let ((case-fold-search nil)
303 (cycled 0) pos)
304 (save-excursion
305 (if (muse-link-at-point)
306 (goto-char (match-end 0)))
307 (while (< cycled 2)
308 (if (re-search-forward
309 (concat "\\(" muse-link-regexp "\\|"
310 muse-url-regexp "\\)") nil t)
311 (setq pos (match-beginning 0)
312 cycled 2)
313 (goto-char (point-min))
314 (setq cycled (1+ cycled)))))
315 (if pos
316 (goto-char pos))))
318 (defun muse-previous-reference ()
319 "Move backward to the next Muse link or URL, cycling if necessary.
320 This function is not entirely accurate, but it's close enough."
321 (interactive)
322 (let ((case-fold-search nil)
323 (cycled 0) pos)
324 (save-excursion
325 (while (< cycled 2)
326 (if (re-search-backward
327 (concat "\\(" muse-link-regexp "\\|"
328 muse-url-regexp "\\)") nil t)
329 (setq pos (point)
330 cycled 2)
331 (goto-char (point-max))
332 (setq cycled (1+ cycled)))))
333 (if pos
334 (goto-char pos))))
336 (defun muse-what-changed ()
337 "Show the unsaved changes that have been made to the current file."
338 (interactive)
339 (diff-backup buffer-file-name))
341 ;;; Generate an index of all known Muse pages
343 (defun muse-generate-index (&optional as-list exclude-private)
344 "Generate an index of all Muse pages."
345 (let ((files (sort (copy-alist (muse-project-file-alist))
346 (function
347 (lambda (l r)
348 (string-lessp (car l) (car r))))))
349 file)
350 (with-current-buffer (get-buffer-create "*Muse Index*")
351 (erase-buffer)
352 (while files
353 (unless (and exclude-private
354 (muse-project-private-p (cdar files)))
355 (insert (if as-list "- " "") "[[" (caar files) "]]\n"))
356 (setq files (cdr files)))
357 (current-buffer))))
359 (defun muse-index ()
360 "Display an index of all known Muse pages."
361 (interactive)
362 (message "Generating Muse index...")
363 (let ((project (muse-project)))
364 (with-current-buffer (muse-generate-index)
365 (goto-char (point-min))
366 (muse-mode)
367 (setq muse-current-project project)
368 (pop-to-buffer (current-buffer))))
369 (message "Generating Muse index...done"))
371 ;;; Insert tags interactively on C-c TAB
373 (defvar muse-tag-history nil
374 "List of recently-entered tags; used by `muse-insert-tag'.
375 If you want a tag to start as the default, you may manually set
376 this variable to a list.")
378 (defvar muse-custom-tags nil
379 "Keep track of any new tags entered in `muse-insert-tag'.
380 If there are (X)HTML tags that you use frequently with that
381 function, you might want to set this manually.")
383 (defun muse-insert-tag (tag)
384 "Insert a tag interactively with a blank line after it."
385 (interactive
386 (list
387 (completing-read
388 (concat "Tag: "
389 (when muse-tag-history
390 (concat "(default: " (car muse-tag-history) ") ")))
391 (nconc (mapcar 'car muse-publish-markup-tags)
392 muse-custom-tags)
393 nil nil nil 'muse-tag-history
394 (car muse-tag-history))))
395 (when (equal tag "")
396 (setq tag (car muse-tag-history)))
397 (let ((tag-entry (assoc tag muse-publish-markup-tags))
398 (options ""))
399 ;; Add to custom list if no entry exists
400 (unless tag-entry
401 (add-to-list 'muse-custom-tags tag))
402 ;; Get option
403 (when (nth 2 tag-entry)
404 (setq options (read-string "Option: ")))
405 (unless (equal options "")
406 (setq options (concat " " options)))
407 ;; Insert the tag, closing if necessary
408 (when tag (insert (concat "<" tag options ">")))
409 (when (nth 1 tag-entry)
410 (insert (concat "\n\n</" tag ">\n"))
411 (forward-line -2))))
413 (provide 'muse-mode)
415 ;;; muse-mode.el ends here