Change mailing address of FSF, add AUTHORS file.
[muse-el.git] / muse-journal.el
blob9b7c4f4824e0d23a5c2451f5980a87830a3853ef
1 ;;; muse-journal.el --- Keep and publish a journal.
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 module facilitates the keeping and publication of a journal.
25 ;; When publishing to HTML, it assumes the form of a web log, or blog.
27 ;; The input format for each entry is as follows:
29 ;; * 20040317: Title of entry
31 ;; Text for the entry.
33 ;; <qotd>
34 ;; "You know who you are. It comes down to a simple gut check: You
35 ;; either love what you do or you don't. Period." -- P. Bronson
36 ;; </qotd>
38 ;; The "qotd", or Quote of the Day, is entirely optional. When
39 ;; generated to HTML, this entry is rendered as:
41 ;; <div id="entry">
42 ;; <div id="entry-qotd">
43 ;; <h3>Quote of the Day:</h3>
44 ;; <p>"You know who you are. It comes down to a simple gut
45 ;; check: You either love what you do or you don't. Period."
46 ;; -- P. Bronson</p>
47 ;; </div>
48 ;; <div id="entry-body">
49 ;; <div id="entry-head">
50 ;; <div id="entry-date">
51 ;; <span class="date">March 17, 2004</span>
52 ;; </div>
53 ;; <div id="entry-title">
54 ;; <h2>Title of entry</h2>
55 ;; </div>
56 ;; </div>
57 ;; <div id="entry-text">
58 ;; <p>Text for the entry.</p>
59 ;; </div>
60 ;; </div>
61 ;; </div>
63 ;; The plurality of "div" tags makes it possible to display the
64 ;; entries in any form you wish, using a CSS style.
66 ;; Also, an .RDF file can be generated from your journal by publishing
67 ;; it with the "rdf" style. It uses the first two sentences of the
68 ;; first paragraph of each entry as its "description", and
69 ;; autogenerates tags for linking to the various entries.
71 ;;; Contributors:
73 ;;; Code:
75 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
77 ;; Muse Journal Publishing
79 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
81 (require 'muse-publish)
82 (require 'muse-html)
83 (require 'muse-latex)
84 (require 'muse-book)
86 (defgroup muse-journal nil
87 "Rules for transforming a journal into its final form."
88 :group 'muse-publish)
90 (defcustom muse-journal-heading-regexp
91 "\\(?:\\([0-9]+\\)\\(?:: \\)?\\)?\\(.+?\\)?"
92 "A regexp that match a journal heading.
93 Paren group 1 is the ISO date, group 2 is the optional category,
94 and group 3 is the optional heading for the entry."
95 :type 'regexp
96 :group 'muse-journal)
98 (defcustom muse-journal-date-format "%a, %e %b %Y"
99 "Date formats to use for journal entries."
100 :type 'string
101 :group 'muse-journal)
103 (defcustom muse-journal-html-heading-regexp
104 (concat "^<h2[^>]*>" muse-journal-heading-regexp "</h2>$")
105 "A regexp that match a journal heading.
106 Paren group 1 is the ISO date, group 2 is the optional category,
107 and group 3 is the optional heading for the entry."
108 :type 'regexp
109 :group 'muse-journal)
111 (defcustom muse-journal-html-entry-template
112 "<div id=\"entry\">
113 <a name=\"%anchor%\" style=\"text-decoration: none\">&nbsp;</a>
114 <div id=\"entry-body\">
115 <div id=\"entry-head\">
116 <div id=\"entry-date\">
117 <span class=\"date\">%date%</span>
118 </div>
119 <div id=\"entry-title\">
120 <h2>%title%</h2>
121 </div>
122 </div>
123 <div id=\"entry-text\">
124 <div id=\"entry-qotd\">
125 <p>%qotd%</p>
126 </div>
127 %text%
128 </div>
129 </div>
130 </div>\n\n"
132 :type 'string
133 :group 'muse-journal)
135 (defcustom muse-journal-latex-section
136 "\\section*{%title% \\hfill {\\normalsize %date%}}
137 \\addcontentsline{toc}{chapter}{%title%}"
139 :type 'string
140 :group 'muse-journal)
142 (defcustom muse-journal-latex-subsection
143 "\\subsection*{%title%}
144 \\addcontentsline{toc}{section}{%title%}"
146 :type 'string
147 :group 'muse-journal)
149 (defcustom muse-journal-latex-markup-tags
150 '(("qotd" t nil muse-journal-latex-qotd-tag))
151 "See `muse-publish-markup-tags' for more info."
152 :type '(repeat (list (string :tag "Markup tag")
153 (boolean :tag "Expect closing tag" :value t)
154 (boolean :tag "Parse attributes" :value nil)
155 function))
156 :group 'muse-journal)
158 (defun muse-journal-generate-pages ()
159 (let ((output-dir (muse-style-element :path)))
160 (goto-char (point-min))
161 (while (re-search-forward muse-journal-heading-regexp nil t)
162 (let* ((date (match-string 1))
163 (category (match-string 1))
164 (category-file (concat output-dir category "/index.html"))
165 (heading (match-string 1)))
166 t))))
168 (defcustom muse-journal-rdf-extension ".rdf"
170 :type 'string
171 :group 'muse-journal)
173 (defcustom muse-journal-rdf-base-url ""
174 "The base URL of the website reference by the RDF file."
175 :type 'string
176 :group 'muse-journal)
178 (defcustom muse-journal-rdf-header
179 "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
180 xmlns=\"http://purl.org/rss/1.0/\"
181 xmlns:dc=\"http://purl.org/dc/elements/1.1/\">
182 <channel rdf:about=\"<lisp>(concat (muse-style-element :base-url)
183 (muse-publish-output-name))</lisp>\">
184 <title><lisp>(muse-publishing-directive \"title\")</lisp></title>
185 <link><lisp>(concat (muse-style-element :base-url)
186 (concat (muse-page-name)
187 muse-html-extension))</lisp></link>
188 <description><lisp>(muse-publishing-directive \"desc\")</lisp></description>
189 <items>
190 <rdf:Seq>
191 <rdf:li resource=\"<lisp>
192 (concat (muse-style-element :base-url)
193 (concat (muse-page-name)
194 muse-html-extension))</lisp>\"/>
195 </rdf:Seq>
196 </items>
197 </channel>\n"
199 :type '(choice string file)
200 :group 'muse-journal)
202 (defcustom muse-journal-rdf-footer
203 "</rdf:RDF>\n"
205 :type '(choice string file)
206 :group 'muse-journal)
208 (defcustom muse-journal-rdf-date-format
209 "%Y-%m-%dT%H:%M:%S"
211 :type 'string
212 :group 'muse-journal)
214 (defcustom muse-journal-rdf-entry-template
215 " <item rdf:about=\"%link%#%anchor%\">
216 <title>%title%</title>
217 <description>
218 %desc%
219 </description>
220 <link>%link%#%anchor%</link>
221 <dc:date>%date%</dc:date>
222 <dc:creator>%maintainer%</dc:creator>
223 </item>\n"
225 :type 'string
226 :group 'muse-journal)
228 (defcustom muse-journal-rdf-summarize-entries t
229 "If non-nil, include only summaries in the RDF file, not the full data."
230 :type 'boolean
231 :group 'muse-journal)
233 (defcustom muse-journal-rss-extension ".xml"
235 :type 'string
236 :group 'muse-journal)
238 (defcustom muse-journal-rss-base-url ""
239 "The base URL of the website reference by the RSS file."
240 :type 'string
241 :group 'muse-journal)
243 (defcustom muse-journal-rss-header
244 "<\?xml version=\"1.0\" encoding=\"<lisp>
245 (muse-html-encoding)</lisp>\"?>
246 <rss version=\"2.0\">
247 <channel>
248 <title><lisp>(muse-publishing-directive \"title\")</lisp></title>
249 <link><lisp>(concat (muse-style-element :base-url)
250 (concat (muse-page-name)
251 muse-html-extension))</lisp></link>
252 <description><lisp>(muse-publishing-directive \"desc\")</lisp></description>
253 <language>en-us</language>
254 <generator>Emacs Muse</generator>"
256 :type '(choice string file)
257 :group 'muse-journal)
259 (defcustom muse-journal-rss-footer
260 " </channel>
261 </rss>\n"
263 :type '(choice string file)
264 :group 'muse-journal)
266 (defcustom muse-journal-rss-date-format
267 "%a, %d %b %Y %H:%M:%S %Z"
269 :type 'string
270 :group 'muse-journal)
272 (defcustom muse-journal-rss-entry-template
273 " <item>
274 <title>%title%</title>
275 <link>%link%#%anchor%</link>
276 <description>%desc%</description>
277 <author><lisp>(muse-publishing-directive \"author\")</lisp></author>
278 <pubDate>%date%</pubDate>
279 <guid>%link%#%anchor%</guid>
280 %enclosure%
281 </item>\n"
283 :type 'string
284 :group 'muse-journal)
286 (defcustom muse-journal-rss-enclosure-types-alist
287 '(("mp3" . "audio/mpeg"))
289 :type '(alist :key-type string :value-type string)
290 :group 'muse-journal)
292 (defcustom muse-journal-rss-summarize-entries nil
293 "If non-nil, include only summaries in the RSS file, not the full data."
294 :type 'boolean
295 :group 'muse-journal)
297 (defcustom muse-journal-rss-markup-regexps
298 '((10000 muse-link-regexp 0 "\\2"))
299 "List of markup rules for publishing a Muse journal page to RDF.
300 For more on the structure of this list, see `muse-publish-markup-regexps'."
301 :type '(repeat (choice
302 (list :tag "Markup rule"
303 integer
304 (choice regexp symbol)
305 integer
306 (choice string function symbol))
307 function))
308 :group 'muse-journal)
310 (defcustom muse-journal-rss-markup-functions
311 '((email . ignore)
312 (link . ignore)
313 (url . ignore))
314 "An alist of style types to custom functions for that kind of text.
315 For more on the structure of this list, see
316 `muse-publish-markup-functions'."
317 :type '(alist :key-type symbol :value-type function)
318 :group 'muse-journal)
320 (defun muse-journal-anchorize-title (title)
321 (save-match-data
322 (if (string-match "(" title)
323 (setq title (substring title 0 (match-beginning 0))))
324 (if (string-match "<[^>]+>" title)
325 (setq title (replace-match "" nil nil title)))
326 (downcase (replace-regexp-in-string "[^a-zA-Z0-9_]" ""
327 title))))
329 (defun muse-journal-sort-entries (&optional direction)
330 (interactive "P")
331 (sort-subr
332 direction
333 (function
334 (lambda ()
335 (if (re-search-forward "^\\* [0-9]+" nil t)
336 (goto-char (match-beginning 0))
337 (goto-char (point-max)))))
338 (function
339 (lambda ()
340 (if (re-search-forward "^\\* [0-9]+" nil t)
341 (goto-char (1- (match-beginning 0)))
342 (goto-char (point-max)))))
343 (function
344 (lambda ()
345 (forward-char 2)))
346 (function
347 (lambda ()
348 (end-of-line)))))
350 (defun muse-journal-html-munge-buffer ()
351 (goto-char (point-min))
352 (let ((heading-regexp muse-journal-html-heading-regexp)
353 (inhibit-read-only t))
354 (while (re-search-forward heading-regexp nil t)
355 (let* ((date (match-string 1))
356 (orig-date date)
357 (title (match-string 2))
358 (clean-title title)
359 datestamp qotd text)
360 (delete-region (match-beginning 0) (match-end 0))
361 (if clean-title
362 (save-match-data
363 (while (string-match "\\(^<[^>]+>\\|<[^>]+>$\\)" clean-title)
364 (setq clean-title (replace-match "" nil nil clean-title)))))
365 (save-match-data
366 (when (and date
367 (string-match
368 (concat "\\`\\([1-9][0-9][0-9][0-9]\\)[./]?"
369 "\\([0-1][0-9]\\)[./]?\\([0-3][0-9]\\)") date))
370 (setq datestamp
371 (encode-time
372 0 0 0
373 (string-to-int (match-string 3 date))
374 (string-to-int (match-string 2 date))
375 (string-to-int (match-string 1 date))
376 (current-time-zone))
377 date (concat (format-time-string
378 muse-journal-date-format datestamp)
379 (substring date (match-end 0))))))
380 (save-restriction
381 (narrow-to-region
382 (point) (if (re-search-forward
383 (concat "\\(^<hr>$\\|"
384 heading-regexp "\\)") nil t)
385 (match-beginning 0)
386 (point-max)))
387 (goto-char (point-max))
388 (while (and (not (bobp))
389 (eq ?\ (char-syntax (char-before))))
390 (delete-char -1))
391 (goto-char (point-min))
392 (while (and (not (eobp))
393 (eq ?\ (char-syntax (char-after))))
394 (delete-char 1))
395 (save-excursion
396 (when (search-forward "<qotd>" nil t)
397 (let ((tag-beg (match-beginning 0))
398 (beg (match-end 0)))
399 (re-search-forward "</qotd>\n*")
400 (setq qotd (buffer-substring-no-properties
401 beg (match-beginning 0)))
402 (delete-region tag-beg (match-end 0)))))
403 (setq text (buffer-string))
404 (delete-region (point-min) (point-max))
405 (let ((entry muse-journal-html-entry-template))
406 (while (string-match "%date%" entry)
407 (setq entry (replace-match (or date "") nil t entry)))
408 (while (string-match "%title%" entry)
409 (setq entry (replace-match (or title "&nbsp;") nil t entry)))
410 (while (string-match "%anchor%" entry)
411 (setq entry (replace-match
412 (muse-journal-anchorize-title
413 (or clean-title orig-date))
414 nil t entry)))
415 (while (string-match "%qotd%" entry)
416 (setq entry (replace-match (or qotd "") nil t entry)))
417 (while (string-match "%text%" entry)
418 (setq entry (replace-match text nil t entry)))
419 (insert entry)
420 (when (null qotd)
421 (goto-char (point-min))
422 (search-forward "<div id=\"entry-qotd\">")
423 (let ((beg (match-beginning 0)))
424 (re-search-forward "</div>\n*")
425 (delete-region beg (point))))))))))
427 (defun muse-journal-latex-munge-buffer ()
428 (goto-char (point-min))
429 (let ((heading-regexp
430 (concat "^" (regexp-quote (muse-markup-text 'section))
431 muse-journal-heading-regexp
432 (regexp-quote (muse-markup-text 'section-end)) "$"))
433 (inhibit-read-only t))
434 (when (re-search-forward heading-regexp nil t)
435 (goto-char (match-beginning 0))
436 (sort-subr nil
437 (function
438 (lambda ()
439 (if (re-search-forward heading-regexp nil t)
440 (goto-char (match-beginning 0))
441 (goto-char (point-max)))))
442 (function
443 (lambda ()
444 (if (re-search-forward heading-regexp nil t)
445 (goto-char (1- (match-beginning 0)))
446 (goto-char (point-max)))))
447 (function
448 (lambda ()
449 (forward-char 2)))
450 (function
451 (lambda ()
452 (end-of-line)))))
453 (while (re-search-forward heading-regexp nil t)
454 (let ((date (match-string 1))
455 (title (match-string 2))
456 qotd section)
457 (save-match-data
458 (when (and date
459 (string-match
460 (concat "\\([1-9][0-9][0-9][0-9]\\)[./]?"
461 "\\([0-1][0-9]\\)[./]?\\([0-3][0-9]\\)") date))
462 (setq date (encode-time
463 0 0 0
464 (string-to-int (match-string 3 date))
465 (string-to-int (match-string 2 date))
466 (string-to-int (match-string 1 date))
467 (current-time-zone))
468 date (format-time-string
469 muse-journal-date-format date))))
470 (save-match-data
471 (setq section muse-journal-latex-section)
472 (while (string-match "%title%" section)
473 (setq section (replace-match (or title "Untitled")
474 nil t section)))
475 (while (string-match "%date%" section)
476 (setq section (replace-match (or date "") nil t section))))
477 (replace-match section nil t))))
478 (goto-char (point-min))
479 (let ((subheading-regexp
480 (concat "^" (regexp-quote (muse-markup-text 'subsection))
481 "\\([^\n}]+\\)"
482 (regexp-quote (muse-markup-text 'subsection-end)) "$"))
483 (inhibit-read-only t))
484 (while (re-search-forward subheading-regexp nil t)
485 (let ((subsection muse-journal-latex-subsection))
486 (save-match-data
487 (let ((title (match-string 1)))
488 (while (string-match "%title%" subsection)
489 (setq subsection (replace-match title nil t subsection)))))
490 (replace-match subsection nil t)))))
492 (defun muse-journal-latex-qotd-tag (beg end)
493 (goto-char beg)
494 (insert (muse-markup-text 'begin-quote))
495 (goto-char end)
496 (insert (muse-markup-text 'end-quote)))
498 (defun muse-journal-rss-munge-buffer ()
499 (goto-char (point-min))
500 (let ((heading-regexp (concat "^\\* " muse-journal-heading-regexp "$"))
501 (inhibit-read-only t))
502 (while (re-search-forward heading-regexp nil t)
503 (let* ((date (match-string 1))
504 (orig-date date)
505 (title (match-string 2))
506 enclosure qotd desc)
507 (if title
508 (save-match-data
509 (if (string-match muse-link-regexp title)
510 (setq enclosure (match-string 1 title)
511 title (match-string 2 title)))))
512 (save-match-data
513 (when (and date
514 (string-match
515 (concat "\\([1-9][0-9][0-9][0-9]\\)[./]?"
516 "\\([0-1][0-9]\\)[./]?\\([0-3][0-9]\\)") date))
517 (setq date (encode-time 0 0 0
518 (string-to-int (match-string 3 date))
519 (string-to-int (match-string 2 date))
520 (string-to-int (match-string 1 date))
521 (current-time-zone))
522 date (format-time-string
523 (muse-style-element :date-format) date))))
524 (save-restriction
525 (narrow-to-region
526 (match-beginning 0)
527 (if (re-search-forward heading-regexp nil t)
528 (match-beginning 0)
529 (if (re-search-forward "^Footnotes:" nil t)
530 (match-beginning 0)
531 (point-max))))
532 (goto-char (point-min))
533 (delete-region (point) (line-end-position))
534 (re-search-forward "</qotd>\n+" nil t)
535 (while (and (char-after)
536 (eq ?\ (char-syntax (char-after))))
537 (delete-char 1))
538 (let ((beg (point)))
539 (if (muse-style-element :summarize)
540 (progn
541 (forward-sentence 2)
542 (setq desc (concat (buffer-substring beg (point)) "...")))
543 (save-restriction
544 (muse-publish-markup-buffer "rss-entry" "html")
545 (goto-char (point-min))
546 (re-search-forward "Page published by Emacs Muse")
547 (goto-char (line-end-position))
548 (setq beg (point))
549 (re-search-forward "Page published by Emacs Muse")
550 (goto-char (line-beginning-position))
551 (setq desc (concat "<![CDATA[" (buffer-substring beg (point))
552 "]]>")))))
553 (delete-region (point-min) (point-max))
554 (let ((entry (muse-style-element :entry-template)))
555 (while (string-match "%date%" entry)
556 (setq entry (replace-match (or date "") nil t entry)))
557 (while (string-match "%title%" entry)
558 (setq entry (replace-match (or title "Untitled") nil t entry)))
559 (while (string-match "%desc%" entry)
560 (setq entry (replace-match desc nil t entry)))
561 (while (string-match "%enclosure%" entry)
562 (setq
563 entry
564 (replace-match
565 (if (null enclosure)
567 (save-match-data
568 (format
569 "<enclosure url=\"%s\" %stype=\"%s\"/>"
570 (if (string-match "//" enclosure)
571 enclosure
572 (concat (muse-style-element :base-url)
573 enclosure))
574 (let ((file
575 (expand-file-name enclosure
576 (muse-style-element :path))))
577 (if (file-readable-p file)
578 (format "length=\"%d\" "
579 (nth 7 (file-attributes file)))
580 ""))
581 (if (string-match "\\.\\([^.]+\\)$" enclosure)
582 (let* ((ext (match-string 1 enclosure))
583 (type
584 (assoc ext muse-journal-rss-enclosure-types-alist)))
585 (if type
586 (cdr type)
587 "application/octet-stream"))))))
588 nil t entry)))
589 (while (string-match "%link%" entry)
590 (setq entry (replace-match
591 (concat (muse-style-element :base-url)
592 (concat (muse-page-name)
593 muse-html-extension))
594 nil t entry)))
595 (while (string-match "%anchor%" entry)
596 (setq entry (replace-match
597 (muse-journal-anchorize-title (or title orig-date))
598 nil t entry)))
599 (while (string-match "%maintainer%" entry)
600 (setq entry (replace-match
601 (or (muse-style-element :maintainer)
602 (concat "webmaster@" (system-name)))
603 nil t entry)))
604 (insert entry)))))
605 (unless (eobp)
606 (delete-region (point) (point-max)))))
608 (unless (assoc "journal-html" muse-publishing-styles)
609 (muse-derive-style "journal-html" "html"
610 :before-end 'muse-journal-html-munge-buffer)
612 (muse-derive-style "journal-xhtml" "xhtml"
613 :before-end 'muse-journal-html-munge-buffer)
615 (muse-derive-style "journal-latex" "latex"
616 :tags 'muse-journal-latex-markup-tags
617 :before-end 'muse-journal-latex-munge-buffer)
619 (muse-derive-style "journal-pdf" "pdf"
620 :tags 'muse-journal-latex-markup-tags
621 :before-end 'muse-journal-latex-munge-buffer)
623 (muse-derive-style "journal-book-latex" "book-latex"
624 ;;:nochapters
625 :tags 'muse-journal-latex-markup-tags
626 :before-end 'muse-journal-latex-munge-buffer)
628 (muse-derive-style "journal-book-pdf" "book-pdf"
629 ;;:nochapters
630 :tags 'muse-journal-latex-markup-tags
631 :before-end 'muse-journal-latex-munge-buffer)
633 (muse-define-style "journal-rdf"
634 :suffix 'muse-journal-rdf-extension
635 :regexps 'muse-journal-rss-markup-regexps
636 :functions 'muse-journal-rss-markup-functions
637 :before 'muse-journal-rss-munge-buffer
638 :header 'muse-journal-rdf-header
639 :footer 'muse-journal-rdf-footer
640 :date-format 'muse-journal-rdf-date-format
641 :entry-template 'muse-journal-rdf-entry-template
642 :base-url 'muse-journal-rdf-base-url
643 :summarize 'muse-journal-rdf-summarize-entries)
645 (muse-define-style "journal-rss"
646 :suffix 'muse-journal-rss-extension
647 :regexps 'muse-journal-rss-markup-regexps
648 :functions 'muse-journal-rss-markup-functions
649 :before 'muse-journal-rss-munge-buffer
650 :header 'muse-journal-rss-header
651 :footer 'muse-journal-rss-footer
652 :date-format 'muse-journal-rss-date-format
653 :entry-template 'muse-journal-rss-entry-template
654 :base-url 'muse-journal-rss-base-url
655 :summarize 'muse-journal-rss-summarize-entries))
657 (provide 'muse-journal)
659 ;;; muse-journal.el ends here