*** empty log message ***
[gnus] / lisp / article.el
1 ;;; article.el --- article treatment functions
2 ;; Copyright (C) 1996 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
5 ;; Keywords: news
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 2, or (at your option)
12 ;; 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; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; Code:
27
28 (require 'nnheader)
29 (require 'gnus-util)
30 (require 'message)
31 (require 'custom)
32
33 (defgroup article nil
34   "Article display."
35   :group 'gnus)
36
37 (defcustom gnus-ignored-headers
38   '("^Path:" "^Posting-Version:" "^Article-I.D.:" "^Expires:"
39     "^Date-Received:" "^References:" "^Control:" "^Xref:" "^Lines:"
40     "^Posted:" "^Relay-Version:" "^Message-ID:" "^Nf-ID:" "^Nf-From:"
41     "^Approved:" "^Sender:" "^Received:" "^Mail-from:") 
42   "All headers that match this regexp will be hidden.
43 This variable can also be a list of regexps of headers to be ignored.
44 If `gnus-visible-headers' is non-nil, this variable will be ignored."
45   :type '(choice :custom-show nil
46                  regexp
47                  (repeat regexp))
48   :group 'article)
49
50 (defcustom gnus-visible-headers 
51   "^From:\\|^Newsgroups:\\|^Subject:\\|^Date:\\|^Followup-To:\\|^Reply-To:\\|^Organization:\\|^Summary:\\|^Keywords:\\|^To:\\|^Cc:\\|^Posted-To:\\|^Mail-Copies-To:\\|^Apparently-To:\\|^Gnus-Warning:\\|^Resent-"
52   "All headers that do not match this regexp will be hidden.
53 This variable can also be a list of regexp of headers to remain visible.
54 If this variable is non-nil, `gnus-ignored-headers' will be ignored."
55   :type '(repeat :value-to-internal (lambda (widget value)
56                                       (custom-split-regexp-maybe value))
57                  :match (lambda (widget value)
58                           (or (stringp value)
59                               (widget-editable-list-match widget value)))
60                  regexp)
61   :group 'article)
62
63 (defcustom gnus-sorted-header-list
64   '("^From:" "^Subject:" "^Summary:" "^Keywords:" "^Newsgroups:" "^To:"
65     "^Cc:" "^Date:" "^Organization:")
66   "This variable is a list of regular expressions.
67 If it is non-nil, headers that match the regular expressions will
68 be placed first in the article buffer in the sequence specified by
69 this list."
70   :type '(repeat regexp)
71   :group 'article)
72
73 (defcustom gnus-boring-article-headers '(empty followup-to reply-to)
74   "Headers that are only to be displayed if they have interesting data.
75 Possible values in this list are `empty', `newsgroups', `followup-to',
76 `reply-to', and `date'."
77   :type '(set (const :tag "Headers with no content." empty)
78               (const :tag "Newsgroups with only one group." newsgroups)
79               (const :tag "Followup-to identical to newsgroups." followup-to)
80               (const :tag "Reply-to identical to from." reply-to)
81               (const :tag "Date less than four days old." date))
82   :group 'article)
83
84 (defcustom gnus-signature-separator '("^-- $" "^-- *$")
85   "Regexp matching signature separator.
86 This can also be a list of regexps.  In that case, it will be checked
87 from head to tail looking for a separator.  Searches will be done from
88 the end of the buffer."
89   :type '(repeat string)
90   :group 'article)
91
92 (defcustom gnus-signature-limit nil
93    "Provide a limit to what is considered a signature.
94 If it is a number, no signature may not be longer (in characters) than
95 that number.  If it is a floating point number, no signature may be
96 longer (in lines) than that number.  If it is a function, the function
97 will be called without any parameters, and if it returns nil, there is
98 no signature in the buffer.  If it is a string, it will be used as a
99 regexp.  If it matches, the text in question is not a signature."
100   :type '(choice integer number function regexp)
101   :group 'article)
102
103 (defcustom gnus-hidden-properties '(invisible t intangible t)
104   "Property list to use for hiding text."
105   :type 'sexp 
106   :group 'article)
107
108 (defcustom gnus-article-x-face-command
109   "{ echo '/* Width=48, Height=48 */'; uncompface; } | icontopbm | xv -quit -"
110   "String or function to be executed to display an X-Face header.
111 If it is a string, the command will be executed in a sub-shell
112 asynchronously.  The compressed face will be piped to this command."
113   :type 'string                         ;Leave function case to Lisp.
114   :group 'article)
115
116 (defcustom gnus-article-x-face-too-ugly nil
117   "Regexp matching posters whose face shouldn't be shown automatically."
118   :type 'regexp
119   :group 'article)
120
121 (defcustom gnus-emphasis-alist
122   (let ((format
123          "\\(\\s-\\|^\\)\\(%s\\(\\w+\\(\\s-+\\w+\\)*\\)%s\\)\\(\\s-\\|[?!.,;)]\\)")
124         (types
125          '(("_" "_" underline)
126            ("/" "/" italic)
127            ("\\*" "\\*" bold)
128            ("_/" "/_" underline-italic)
129            ("_\\*" "\\*_" underline-bold)
130            ("\\*/" "/\\*" bold-italic)
131            ("_\\*/" "/\\*_" underline-bold-italic))))
132     `(("\\(\\s-\\|^\\)\\(_\\(\\(\\w\\|_\\)+\\)_\\)\\(\\s-\\|[?!.,;]\\)"
133        2 3 'gnus-emphasis-underline)
134       ,@(mapcar
135          (lambda (spec)
136            (list
137             (format format (car spec) (cadr spec))
138             2 3 (intern (format "gnus-emphasis-%s" (caddr spec)))))
139          types)))
140   "Alist that says how to fontify certain phrases.
141 Each item looks like this:
142
143   (\"_\\\\([[\\w+\\\\)_\" 0 1 'underline)
144
145 The first element is a regular expression to be matched.  The second
146 is a number that says what regular expression grouping used to find
147 the entire emphasized word.  The third is a number that says what
148 regexp grouping should be displayed and highlighted.  The fourth
149 is the face used for highlighting."
150   :type '(repeat (list :value ("" 0 0 default)
151                        regexp
152                        (integer :tag "Match group")
153                        (integer :tag "Emphasize group")
154                        face))
155   :group 'article)
156
157 (defface gnus-emphasis-bold '((t (:bold t)))
158   "Face used for displaying strong emphasized text (*word*)."
159   :group 'article)
160
161 (defface gnus-emphasis-italic '((t (:italic t)))
162   "Face used for displaying italic emphasized text (/word/)."
163   :group 'article)
164
165 (defface gnus-emphasis-underline '((t (:underline t)))
166   "Face used for displaying underlined emphasized text (_word_)."
167   :group 'article)
168
169 (defface gnus-emphasis-bold-underline '((t (:bold t :underline t)))
170   "Face used for displaying underlined bold emphasized text (_*word*_)."
171   :group 'article)
172
173 (defface gnus-emphasis-underline-italic '((t (:italic t :underline t)))
174   "Face used for displaying underlined italic emphasized text (_*word*_)."
175   :group 'article)
176
177 (defface gnus-emphasis-bold-italic '((t (:bold t :italic t)))
178   "Face used for displaying bold italic emphasized text (/*word*/)."
179   :group 'article)
180
181 (defface gnus-emphasis-underline-bold-italic 
182   '((t (:bold t :italic t :underline t)))
183   "Face used for displaying underlined bold italic emphasized text (_/*word*/_)."
184   :group 'article)
185
186 (eval-and-compile
187   (autoload 'hexl-hex-string-to-integer "hexl")
188   (autoload 'timezone-make-date-arpa-standard "timezone")
189   (autoload 'mail-extract-address-components "mail-extr"))
190
191 ;;; Internal variables.
192
193 (defvar gnus-inhibit-hiding nil)
194 (defvar gnus-newsgroup-name)
195
196 (defsubst article-hide-text (b e props)
197   "Set text PROPS on the B to E region, extending `intangible' 1 past B."
198   (add-text-properties b e props)
199   (when (memq 'intangible props)
200     (put-text-property 
201      (max (1- b) (point-min))
202      b 'intangible (cddr (memq 'intangible props)))))
203
204 (defsubst article-unhide-text (b e)
205   "Remove hidden text properties from region between B and E."
206   (remove-text-properties b e gnus-hidden-properties)
207   (when (memq 'intangible gnus-hidden-properties)
208     (put-text-property (max (1- b) (point-min))
209                        b 'intangible nil)))
210
211 (defun article-hide-text-type (b e type)
212   "Hide text of TYPE between B and E."
213   (article-hide-text
214    b e (cons 'article-type (cons type gnus-hidden-properties))))
215
216 (defun article-unhide-text-type (b e type)
217   "Hide text of TYPE between B and E."
218   (remove-text-properties
219    b e (cons 'article-type (cons type gnus-hidden-properties)))
220   (when (memq 'intangible gnus-hidden-properties)
221     (put-text-property (max (1- b) (point-min))
222                        b 'intangible nil)))
223
224 (defun article-hide-text-of-type (type)
225   "Hide text of TYPE in the current buffer."
226   (save-excursion
227     (let ((b (point-min))
228           (e (point-max)))
229       (while (setq b (text-property-any b e 'article-type type))
230         (add-text-properties b (incf b) gnus-hidden-properties)))))
231
232 (defun article-delete-text-of-type (type)
233   "Delete text of TYPE in the current buffer."
234   (save-excursion
235     (let ((b (point-min))
236           (e (point-max)))
237       (while (setq b (text-property-any b e 'article-type type))
238         (delete-region b (incf b))))))
239
240 (defun article-text-type-exists-p (type)
241   "Say whether any text of type TYPE exists in the buffer."
242   (text-property-any (point-min) (point-max) 'article-type type))
243
244 (defsubst article-header-rank ()
245   "Give the rank of the string HEADER as given by `article-sorted-header-list'."
246   (let ((list gnus-sorted-header-list)
247         (i 0))
248     (while list
249       (when (looking-at (car list))
250         (setq list nil))
251       (setq list (cdr list))
252       (incf i))
253     i))
254
255 (defun article-hide-headers (&optional arg delete)
256   "Toggle whether to hide unwanted headers and possibly sort them as well.
257 If given a negative prefix, always show; if given a positive prefix,
258 always hide."
259   (interactive (article-hidden-arg))
260   (if (article-check-hidden-text 'headers arg)
261       ;; Show boring headers as well.
262       (article-show-hidden-text 'boring-headers)
263     ;; This function might be inhibited.
264     (unless gnus-inhibit-hiding
265       (save-excursion
266         (save-restriction
267           (let ((buffer-read-only nil)
268                 (props (nconc (list 'article-type 'headers)
269                               gnus-hidden-properties))
270                 (max (1+ (length gnus-sorted-header-list)))
271                 (ignored (when (not gnus-visible-headers)
272                            (cond ((stringp gnus-ignored-headers)
273                                   gnus-ignored-headers)
274                                  ((listp gnus-ignored-headers)
275                                   (mapconcat 'identity gnus-ignored-headers
276                                              "\\|")))))
277                 (visible
278                  (cond ((stringp gnus-visible-headers)
279                         gnus-visible-headers)
280                        ((and gnus-visible-headers
281                              (listp gnus-visible-headers))
282                         (mapconcat 'identity gnus-visible-headers "\\|"))))
283                 (inhibit-point-motion-hooks t)
284                 want-list beg)
285             ;; First we narrow to just the headers.
286             (widen)
287             (goto-char (point-min))
288             ;; Hide any "From " lines at the beginning of (mail) articles.
289             (while (looking-at "From ")
290               (forward-line 1))
291             (unless (bobp)
292               (if delete
293                   (delete-region (point-min) (point))
294                 (article-hide-text (point-min) (point) props)))
295             ;; Then treat the rest of the header lines.
296             (narrow-to-region
297              (point)
298              (if (search-forward "\n\n" nil t) ; if there's a body
299                  (progn (forward-line -1) (point))
300                (point-max)))
301             ;; Then we use the two regular expressions
302             ;; `gnus-ignored-headers' and `gnus-visible-headers' to
303             ;; select which header lines is to remain visible in the
304             ;; article buffer.
305             (goto-char (point-min))
306             (while (re-search-forward "^[^ \t]*:" nil t)
307               (beginning-of-line)
308               ;; Mark the rank of the header.
309               (put-text-property 
310                (point) (1+ (point)) 'message-rank
311                (if (or (and visible (looking-at visible))
312                        (and ignored
313                             (not (looking-at ignored))))
314                    (article-header-rank) 
315                  (+ 2 max)))
316               (forward-line 1))
317             (message-sort-headers-1)
318             (when (setq beg (text-property-any 
319                              (point-min) (point-max) 'message-rank (+ 2 max)))
320               ;; We make the unwanted headers invisible.
321               (if delete
322                   (delete-region beg (point-max))
323                 ;; Suggested by Sudish Joseph <joseph@cis.ohio-state.edu>.
324                 (article-hide-text-type beg (point-max) 'headers))
325               ;; Work around XEmacs lossage.
326               (put-text-property (point-min) beg 'invisible nil))))))))
327
328 (defun article-hide-boring-headers (&optional arg)
329   "Toggle hiding of headers that aren't very interesting.
330 If given a negative prefix, always show; if given a positive prefix,
331 always hide."
332   (interactive (article-hidden-arg))
333   (unless (article-check-hidden-text 'boring-headers arg)
334     (save-excursion
335       (save-restriction
336         (let ((buffer-read-only nil)
337               (list gnus-boring-article-headers)
338               (inhibit-point-motion-hooks t)
339               elem)
340           (nnheader-narrow-to-headers)
341           (while list
342             (setq elem (pop list))
343             (goto-char (point-min))
344             (cond
345              ;; Hide empty headers.
346              ((eq elem 'empty)
347               (while (re-search-forward "^[^:]+:[ \t]*\n[^ \t]" nil t)
348                 (forward-line -1)
349                 (article-hide-text-type
350                  (progn (beginning-of-line) (point))
351                  (progn 
352                    (end-of-line)
353                    (if (re-search-forward "^[^ \t]" nil t)
354                        (match-beginning 0)
355                      (point-max)))
356                  'boring-headers)))
357              ;; Hide boring Newsgroups header.
358              ((eq elem 'newsgroups)
359               (when (equal (gnus-fetch-field "newsgroups")
360                            (gnus-group-real-name
361                             (if (boundp 'gnus-newsgroup-name)
362                                 gnus-newsgroup-name
363                               "")))
364                 (article-hide-header "newsgroups")))
365              ((eq elem 'followup-to)
366               (when (equal (message-fetch-field "followup-to")
367                            (message-fetch-field "newsgroups"))
368                 (article-hide-header "followup-to")))
369              ((eq elem 'reply-to)
370               (let ((from (message-fetch-field "from"))
371                     (reply-to (message-fetch-field "reply-to")))
372                 (when (and
373                        from reply-to
374                        (equal 
375                         (nth 1 (mail-extract-address-components from))
376                         (nth 1 (mail-extract-address-components reply-to))))
377                   (article-hide-header "reply-to"))))
378              ((eq elem 'date)
379               (let ((date (message-fetch-field "date")))
380                 (when (and date
381                            (< (gnus-days-between (current-time-string) date)
382                               4))
383                   (article-hide-header "date")))))))))))
384
385 (defun article-hide-header (header)
386   (save-excursion
387     (goto-char (point-min))
388     (when (re-search-forward (concat "^" header ":") nil t)
389       (article-hide-text-type
390        (progn (beginning-of-line) (point))
391        (progn 
392          (end-of-line)
393          (if (re-search-forward "^[^ \t]" nil t)
394              (match-beginning 0)
395            (point-max)))
396        'boring-headers))))
397
398 ;; Written by Per Abrahamsen <amanda@iesd.auc.dk>.
399 (defun article-treat-overstrike ()
400   "Translate overstrikes into bold text."
401   (interactive)
402   (save-excursion
403     (let ((buffer-read-only nil))
404       (while (search-forward "\b" nil t)
405         (let ((next (following-char))
406               (previous (char-after (- (point) 2))))
407           ;; We do the boldification/underlining by hiding the
408           ;; overstrikes and putting the proper text property
409           ;; on the letters.
410           (cond 
411            ((eq next previous)
412             (article-hide-text-type (- (point) 2) (point) 'overstrike)
413             (put-text-property (point) (1+ (point)) 'face 'bold))
414            ((eq next ?_)
415             (article-hide-text-type (1- (point)) (1+ (point)) 'overstrike)
416             (put-text-property
417              (- (point) 2) (1- (point)) 'face 'underline))
418            ((eq previous ?_)
419             (article-hide-text-type (- (point) 2) (point) 'overstrike)
420             (put-text-property
421              (point) (1+ (point)) 'face 'underline))))))))
422
423 (defun article-fill ()
424   "Format too long lines."
425   (interactive)
426   (save-excursion
427     (let ((buffer-read-only nil))
428       (widen)
429       (goto-char (point-min))
430       (search-forward "\n\n" nil t)
431       (end-of-line 1)
432       (let ((paragraph-start "^[>|#:<;* ]*[ \t]*$")
433             (adaptive-fill-regexp "[ \t]*\\([|#:<;>*]+ *\\)?")
434             (adaptive-fill-mode t))
435         (while (not (eobp))
436           (and (>= (current-column) (min fill-column (window-width)))
437                (/= (preceding-char) ?:)
438                (fill-paragraph nil))
439           (end-of-line 2))))))
440
441 (defun article-remove-cr ()
442   "Remove carriage returns from an article."
443   (interactive)
444   (save-excursion
445     (let ((buffer-read-only nil))
446       (goto-char (point-min))
447       (while (search-forward "\r" nil t)
448         (replace-match "" t t)))))
449
450 (defun article-remove-trailing-blank-lines ()
451   "Remove all trailing blank lines from the article."
452   (interactive)
453   (save-excursion
454     (let ((buffer-read-only nil))
455       (goto-char (point-max))
456       (delete-region
457        (point)
458        (progn
459          (while (looking-at "^[ \t]*$")
460            (forward-line -1))
461          (forward-line 1)
462          (point))))))
463
464 (defun article-display-x-face (&optional force)
465   "Look for an X-Face header and display it if present."
466   (interactive (list 'force))
467   (save-excursion
468     ;; Delete the old process, if any.
469     (when (process-status "article-x-face")
470       (delete-process "article-x-face"))
471     (let ((inhibit-point-motion-hooks t)
472           (case-fold-search nil)
473           from)
474       (save-restriction
475         (nnheader-narrow-to-headers)
476         (setq from (message-fetch-field "from"))
477         (goto-char (point-min))
478         (when (and gnus-article-x-face-command
479                    (or force
480                        ;; Check whether this face is censored.
481                        (not gnus-article-x-face-too-ugly)
482                        (and gnus-article-x-face-too-ugly from
483                             (not (string-match gnus-article-x-face-too-ugly
484                                                from))))
485                    ;; Has to be present.
486                    (re-search-forward "^X-Face: " nil t))
487           ;; We now have the area of the buffer where the X-Face is stored.
488           (let ((beg (point))
489                 (end (1- (re-search-forward "^\\($\\|[^ \t]\\)" nil t))))
490             ;; We display the face.
491             (if (symbolp gnus-article-x-face-command)
492                 ;; The command is a lisp function, so we call it.
493                 (if (gnus-functionp gnus-article-x-face-command)
494                     (funcall gnus-article-x-face-command beg end)
495                   (error "%s is not a function" gnus-article-x-face-command))
496               ;; The command is a string, so we interpret the command
497               ;; as a, well, command, and fork it off.
498               (let ((process-connection-type nil))
499                 (process-kill-without-query
500                  (start-process
501                   "article-x-face" nil shell-file-name shell-command-switch
502                   gnus-article-x-face-command))
503                 (process-send-region "article-x-face" beg end)
504                 (process-send-eof "article-x-face")))))))))
505
506 (defun article-decode-rfc1522 ()
507   "Hack to remove QP encoding from headers."
508   (let ((case-fold-search t)
509         (inhibit-point-motion-hooks t)
510         (buffer-read-only nil)
511         string)
512     (save-restriction
513       (narrow-to-region
514        (goto-char (point-min))
515        (or (search-forward "\n\n" nil t) (point-max)))
516       (goto-char (point-min))
517       (while (re-search-forward 
518               "=\\?iso-8859-1\\?q\\?\\([^?\t\n]*\\)\\?=" nil t)
519         (setq string (match-string 1))
520         (save-restriction
521           (narrow-to-region (match-beginning 0) (match-end 0))
522           (delete-region (point-min) (point-max))
523           (insert string)
524           (article-mime-decode-quoted-printable 
525            (goto-char (point-min)) (point-max))
526           (subst-char-in-region (point-min) (point-max) ?_ ? )
527           (goto-char (point-max)))
528         (when (looking-at "\\([ \t\n]+\\)=\\?")
529           (replace-match "" t t nil 1))
530         (goto-char (point-min))))))
531
532 (defun article-de-quoted-unreadable (&optional force)
533   "Do a naive translation of a quoted-printable-encoded article.
534 This is in no way, shape or form meant as a replacement for real MIME
535 processing, but is simply a stop-gap measure until MIME support is
536 written.
537 If FORCE, decode the article whether it is marked as quoted-printable
538 or not."
539   (interactive (list 'force))
540   (save-excursion
541     (let ((case-fold-search t)
542           (buffer-read-only nil)
543           (type (gnus-fetch-field "content-transfer-encoding")))
544       (article-decode-rfc1522)
545       (when (or force
546                 (and type (string-match "quoted-printable" (downcase type))))
547         (goto-char (point-min))
548         (search-forward "\n\n" nil 'move)
549         (article-mime-decode-quoted-printable (point) (point-max))))))
550
551 (defun article-mime-decode-quoted-printable-buffer ()
552   "Decode Quoted-Printable in the current buffer."
553   (article-mime-decode-quoted-printable (point-min) (point-max)))
554   
555 (defun article-mime-decode-quoted-printable (from to)
556   "Decode Quoted-Printable in the region between FROM and TO."
557   (interactive "r")
558   (goto-char from)
559   (while (search-forward "=" to t)
560     (cond ((eq (following-char) ?\n)
561            (delete-char -1)
562            (delete-char 1))
563           ((looking-at "[0-9A-F][0-9A-F]")
564            (subst-char-in-region
565             (1- (point)) (point) ?=
566             (hexl-hex-string-to-integer
567              (buffer-substring (point) (+ 2 (point)))))
568            (delete-char 2))
569           ((looking-at "=")
570            (delete-char 1))
571           ((gnus-message 3 "Malformed MIME quoted-printable message")))))
572
573 (defun article-hide-pgp (&optional arg)
574   "Toggle hiding of any PGP headers and signatures in the current article.
575 If given a negative prefix, always show; if given a positive prefix,
576 always hide."
577   (interactive (article-hidden-arg))
578   (unless (article-check-hidden-text 'pgp arg)
579     (save-excursion
580       (let (buffer-read-only beg end)
581         (widen)
582         (goto-char (point-min))
583         ;; Hide the "header".
584         (and (search-forward "\n-----BEGIN PGP SIGNED MESSAGE-----\n" nil t)
585              (article-hide-text-type (match-beginning 0) (match-end 0) 'pgp))
586         (setq beg (point))
587         ;; Hide the actual signature.
588         (and (search-forward "\n-----BEGIN PGP SIGNATURE-----\n" nil t)
589              (setq end (1+ (match-beginning 0)))
590              (article-hide-text-type
591               end
592               (if (search-forward "\n-----END PGP SIGNATURE-----\n" nil t)
593                   (match-end 0)
594                 ;; Perhaps we shouldn't hide to the end of the buffer
595                 ;; if there is no end to the signature?
596                 (point-max))
597               'pgp))
598         ;; Hide "- " PGP quotation markers.
599         (when (and beg end)
600           (narrow-to-region beg end)
601           (goto-char (point-min))
602           (while (re-search-forward "^- " nil t)
603             (article-hide-text-type (match-beginning 0) (match-end 0) 'pgp))
604           (widen))))))
605
606 (defun article-hide-pem (&optional arg)
607   "Toggle hiding of any PEM headers and signatures in the current article.
608 If given a negative prefix, always show; if given a positive prefix,
609 always hide."
610   (interactive (article-hidden-arg))
611   (unless (article-check-hidden-text 'pem arg)
612     (save-excursion
613       (let (buffer-read-only end)
614         (widen)
615         (goto-char (point-min))
616         ;; hide the horrendously ugly "header".
617         (and (search-forward "\n-----BEGIN PRIVACY-ENHANCED MESSAGE-----\n"
618                              nil
619                              t)
620              (setq end (1+ (match-beginning 0)))
621              (article-hide-text-type
622               end
623               (if (search-forward "\n\n" nil t)
624                   (match-end 0)
625                 (point-max))
626               'pem))
627         ;; hide the trailer as well
628         (and (search-forward "\n-----END PRIVACY-ENHANCED MESSAGE-----\n"
629                              nil
630                              t)
631              (article-hide-text-type
632               (match-beginning 0) (match-end 0) 'pem))))))
633
634 (defun article-hide-signature (&optional arg)
635   "Hide the signature in the current article.
636 If given a negative prefix, always show; if given a positive prefix,
637 always hide."
638   (interactive (article-hidden-arg))
639   (unless (article-check-hidden-text 'signature arg)
640     (save-excursion
641       (save-restriction
642         (let ((buffer-read-only nil))
643           (when (article-narrow-to-signature)
644             (article-hide-text-type (point-min) (point-max) 'signature)))))))
645
646 (defun article-strip-leading-blank-lines ()
647   "Remove all blank lines from the beginning of the article."
648   (interactive)
649   (save-excursion
650     (let (buffer-read-only)
651       (goto-char (point-min))
652       (when (search-forward "\n\n" nil t)
653         (while (and (not (eobp))
654                     (looking-at "[ \t]*$"))
655           (gnus-delete-line))))))
656
657 (defun article-strip-multiple-blank-lines ()
658   "Replace consecutive blank lines with one empty line."
659   (interactive)
660   (save-excursion
661     (let (buffer-read-only)
662       ;; First make all blank lines empty.
663       (goto-char (point-min))
664       (while (re-search-forward "^[ \t]+$" nil t)
665         (replace-match "" nil t))
666       ;; Then replace multiple empty lines with a single empty line.
667       (goto-char (point-min))
668       (while (re-search-forward "\n\n\n+" nil t)
669         (replace-match "\n\n" t t)))))
670
671 (defun article-strip-blank-lines ()
672   "Strip leading, trailing and multiple blank lines."
673   (interactive)
674   (article-strip-leading-blank-lines)
675   (article-remove-trailing-blank-lines)
676   (article-strip-multiple-blank-lines))
677
678 (defvar mime::preview/content-list)
679 (defvar mime::preview-content-info/point-min)
680 (defun article-narrow-to-signature ()
681   "Narrow to the signature; return t if a signature is found, else nil."
682   (widen)
683   (when (and (boundp 'mime::preview/content-list)
684              mime::preview/content-list)
685     ;; We have a MIMEish article, so we use the MIME data to narrow.
686     (let ((pcinfo (car (last mime::preview/content-list))))
687       (ignore-errors
688         (narrow-to-region
689          (funcall (intern "mime::preview-content-info/point-min") pcinfo)
690          (point-max)))))
691   
692   (when (article-search-signature)
693     (forward-line 1)
694     ;; Check whether we have some limits to what we consider
695     ;; to be a signature.
696     (let ((limits (if (listp gnus-signature-limit) gnus-signature-limit
697                     (list gnus-signature-limit)))
698           limit limited)
699       (while (setq limit (pop limits))
700         (if (or (and (integerp limit)
701                      (< (- (point-max) (point)) limit))
702                 (and (floatp limit)
703                      (< (count-lines (point) (point-max)) limit))
704                 (and (gnus-functionp limit)
705                      (funcall limit))
706                 (and (stringp limit)
707                      (not (re-search-forward limit nil t))))
708             ()                          ; This limit did not succeed.
709           (setq limited t
710                 limits nil)))
711       (unless limited
712         (narrow-to-region (point) (point-max))
713         t))))
714
715 (defun article-search-signature ()
716   "Search the current buffer for the signature separator.
717 Put point at the beginning of the signature separator."
718   (let ((cur (point)))
719     (goto-char (point-max))
720     (if (if (stringp gnus-signature-separator)
721             (re-search-backward gnus-signature-separator nil t)
722           (let ((seps gnus-signature-separator))
723             (while (and seps
724                         (not (re-search-backward (car seps) nil t)))
725               (pop seps))
726             seps))
727         t
728       (goto-char cur)
729       nil)))
730
731 (defun article-hidden-arg ()
732   "Return the current prefix arg as a number, or 0 if no prefix."
733   (list (if current-prefix-arg
734             (prefix-numeric-value current-prefix-arg)
735           0)))
736
737 (defun article-check-hidden-text (type arg)
738   "Return nil if hiding is necessary.
739 Arg can be nil or a number.  Nil and positive means hide, negative
740 means show, 0 means toggle."
741   (save-excursion
742     (let ((hide (article-hidden-text-p type)))
743       (cond
744        ((or (null arg)
745             (> arg 0))
746         nil)
747        ((< arg 0)
748         (article-show-hidden-text type))
749        (t
750         (if (eq hide 'hidden)
751             (article-show-hidden-text type)
752           nil))))))
753
754 (defun article-hidden-text-p (type)
755   "Say whether the current buffer contains hidden text of type TYPE."
756   (let ((pos (text-property-any (point-min) (point-max) 'article-type type)))
757     (when pos
758       (if (get-text-property pos 'invisible)
759           'hidden
760         'shown))))
761
762 (defun article-show-hidden-text (type &optional hide)
763   "Show all hidden text of type TYPE.
764 If HIDE, hide the text instead."
765   (save-excursion
766     (let ((buffer-read-only nil)
767           (inhibit-point-motion-hooks t)
768           (beg (point-min)))
769       (while (gnus-goto-char (text-property-any
770                               beg (point-max) 'article-type type))
771         (setq beg (point))
772         (forward-char)
773         (if hide
774             (article-hide-text beg (point) gnus-hidden-properties)
775           (article-unhide-text beg (point)))
776         (setq beg (point)))
777       t)))
778
779 (defconst article-time-units
780   `((year . ,(* 365.25 24 60 60))
781     (week . ,(* 7 24 60 60))
782     (day . ,(* 24 60 60))
783     (hour . ,(* 60 60))
784     (minute . 60)
785     (second . 1))
786   "Mapping from time units to seconds.")
787
788 (defun article-date-ut (&optional type highlight header)
789   "Convert DATE date to universal time in the current article.
790 If TYPE is `local', convert to local time; if it is `lapsed', output
791 how much time has lapsed since DATE."
792   (interactive (list 'ut t))
793   (let* ((header (or header (message-fetch-field "date") ""))
794          (date (if (vectorp header) (mail-header-date header)
795                  header))
796          (date-regexp "^Date: \\|^X-Sent: ")
797          (inhibit-point-motion-hooks t)
798          bface eface)
799     (when (and date (not (string= date "")))
800       (save-excursion
801         (save-restriction
802           (nnheader-narrow-to-headers)
803           (let ((buffer-read-only nil))
804             ;; Delete any old Date headers.
805             (if (re-search-forward date-regexp nil t)
806                 (progn
807                   (setq bface (get-text-property (gnus-point-at-bol) 'face)
808                         eface (get-text-property (1- (gnus-point-at-eol))
809                                                  'face))
810                   (message-remove-header date-regexp t)
811                   (beginning-of-line))
812               (goto-char (point-max)))
813             (insert (article-make-date-line date type))
814             ;; Do highlighting.
815             (forward-line -1)
816             (when (looking-at "\\([^:]+\\): *\\(.*\\)$")
817               (put-text-property (match-beginning 1) (match-end 1)
818                                  'face bface)
819               (put-text-property (match-beginning 2) (match-end 2)
820                                  'face eface))))))))
821
822 (defun article-make-date-line (date type)
823   "Return a DATE line of TYPE."
824   (cond
825    ;; Convert to the local timezone.  We have to slap a
826    ;; `condition-case' round the calls to the timezone
827    ;; functions since they aren't particularly resistant to
828    ;; buggy dates.
829    ((eq type 'local)
830     (concat "Date: " (condition-case ()
831                          (timezone-make-date-arpa-standard date)
832                        (error date))
833             "\n"))
834    ;; Convert to Universal Time.
835    ((eq type 'ut)
836     (concat "Date: "
837             (condition-case ()
838                 (timezone-make-date-arpa-standard date nil "UT")
839               (error date))
840             "\n"))
841    ;; Get the original date from the article.
842    ((eq type 'original)
843     (concat "Date: " date "\n"))
844    ;; Do an X-Sent lapsed format.
845    ((eq type 'lapsed)
846     ;; If the date is seriously mangled, the timezone functions are
847     ;; liable to bug out, so we ignore all errors.
848     (let* ((now (current-time))
849            (real-time
850             (ignore-errors
851               (gnus-time-minus
852                (gnus-encode-date
853                 (timezone-make-date-arpa-standard
854                  (current-time-string now)
855                  (current-time-zone now) "UT"))
856                (gnus-encode-date
857                 (timezone-make-date-arpa-standard
858                  date nil "UT")))))
859            (real-sec (and real-time
860                           (+ (* (float (car real-time)) 65536)
861                              (cadr real-time))))
862            (sec (and real-time (abs real-sec)))
863            num prev)
864       (cond
865        ((null real-time)
866         "X-Sent: Unknown\n")
867        ((zerop sec)
868         "X-Sent: Now\n")
869        (t
870         (concat
871          "X-Sent: "
872          ;; This is a bit convoluted, but basically we go
873          ;; through the time units for years, weeks, etc,
874          ;; and divide things to see whether that results
875          ;; in positive answers.
876          (mapconcat
877           (lambda (unit)
878             (if (zerop (setq num (ffloor (/ sec (cdr unit)))))
879                 ;; The (remaining) seconds are too few to
880                 ;; be divided into this time unit.
881                 ""
882               ;; It's big enough, so we output it.
883               (setq sec (- sec (* num (cdr unit))))
884               (prog1
885                   (concat (if prev ", " "") (int-to-string
886                                              (floor num))
887                           " " (symbol-name (car unit)) 
888                           (if (> num 1) "s" ""))
889                 (setq prev t))))
890           article-time-units "")
891          ;; If dates are odd, then it might appear like the
892          ;; article was sent in the future.
893          (if (> real-sec 0)
894              " ago\n"
895            " in the future\n"))))))
896    (t
897     (error "Unknown conversion type: %s" type))))
898
899 (defun article-date-local (&optional highlight)
900   "Convert the current article date to the local timezone."
901   (interactive (list t))
902   (article-date-ut 'local highlight))
903
904 (defun article-date-original (&optional highlight)
905   "Convert the current article date to what it was originally.
906 This is only useful if you have used some other date conversion
907 function and want to see what the date was before converting."
908   (interactive (list t))
909   (article-date-ut 'original highlight))
910
911 (defun article-date-lapsed (&optional highlight)
912   "Convert the current article date to time lapsed since it was sent."
913   (interactive (list t))
914   (article-date-ut 'lapsed highlight))
915
916 (defun article-show-all ()
917   "Show all hidden text in the article buffer."
918   (interactive)
919   (save-excursion
920     (let ((buffer-read-only nil))
921       (article-unhide-text (point-min) (point-max)))))
922
923 (defun article-emphasize (&optional arg)
924   "Emphasize text according to `gnus-emphasis-alist'."
925   (interactive (article-hidden-arg))
926   (unless (article-check-hidden-text 'emphasis arg)
927     (save-excursion
928       (let ((alist gnus-emphasis-alist)
929             (buffer-read-only nil)
930             (props (append '(article-type emphasis)
931                            gnus-hidden-properties))
932             regexp elem beg invisible visible face)
933         (goto-char (point-min))
934         (search-forward "\n\n" nil t)
935         (setq beg (point))
936         (while (setq elem (pop alist))
937           (goto-char beg)
938           (setq regexp (car elem)
939                 invisible (nth 1 elem)
940                 visible (nth 2 elem)
941                 face (nth 3 elem))
942           (while (re-search-forward regexp nil t)
943             (when (and (match-beginning visible) (match-beginning invisible))
944               (article-hide-text
945                (match-beginning invisible) (match-end invisible) props)
946               (article-unhide-text-type
947                (match-beginning visible) (match-end visible) 'emphasis)
948               (put-text-property 
949                (match-beginning visible) (match-end visible) 'face face)
950               (goto-char (match-end invisible)))))))))
951
952 (provide 'article)
953
954 ;;; article.el ends here