*** 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 'custom)
29 (require 'nnheader)
30 (require 'gnus-util)
31 (require 'message)
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-underline-bold '((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 (and (not (bobp))
460                      (looking-at "^[ \t]*$"))
461            (forward-line -1))
462          (forward-line 1)
463          (point))))))
464
465 (defun article-display-x-face (&optional force)
466   "Look for an X-Face header and display it if present."
467   (interactive (list 'force))
468   (save-excursion
469     ;; Delete the old process, if any.
470     (when (process-status "article-x-face")
471       (delete-process "article-x-face"))
472     (let ((inhibit-point-motion-hooks t)
473           (case-fold-search nil)
474           from)
475       (save-restriction
476         (nnheader-narrow-to-headers)
477         (setq from (message-fetch-field "from"))
478         (goto-char (point-min))
479         (when (and gnus-article-x-face-command
480                    (or force
481                        ;; Check whether this face is censored.
482                        (not gnus-article-x-face-too-ugly)
483                        (and gnus-article-x-face-too-ugly from
484                             (not (string-match gnus-article-x-face-too-ugly
485                                                from))))
486                    ;; Has to be present.
487                    (re-search-forward "^X-Face: " nil t))
488           ;; We now have the area of the buffer where the X-Face is stored.
489           (let ((beg (point))
490                 (end (1- (re-search-forward "^\\($\\|[^ \t]\\)" nil t))))
491             ;; We display the face.
492             (if (symbolp gnus-article-x-face-command)
493                 ;; The command is a lisp function, so we call it.
494                 (if (gnus-functionp gnus-article-x-face-command)
495                     (funcall gnus-article-x-face-command beg end)
496                   (error "%s is not a function" gnus-article-x-face-command))
497               ;; The command is a string, so we interpret the command
498               ;; as a, well, command, and fork it off.
499               (let ((process-connection-type nil))
500                 (process-kill-without-query
501                  (start-process
502                   "article-x-face" nil shell-file-name shell-command-switch
503                   gnus-article-x-face-command))
504                 (process-send-region "article-x-face" beg end)
505                 (process-send-eof "article-x-face")))))))))
506
507 (defun article-decode-rfc1522 ()
508   "Hack to remove QP encoding from headers."
509   (let ((case-fold-search t)
510         (inhibit-point-motion-hooks t)
511         (buffer-read-only nil)
512         string)
513     (save-restriction
514       (narrow-to-region
515        (goto-char (point-min))
516        (or (search-forward "\n\n" nil t) (point-max)))
517       (goto-char (point-min))
518       (while (re-search-forward 
519               "=\\?iso-8859-1\\?q\\?\\([^?\t\n]*\\)\\?=" nil t)
520         (setq string (match-string 1))
521         (save-restriction
522           (narrow-to-region (match-beginning 0) (match-end 0))
523           (delete-region (point-min) (point-max))
524           (insert string)
525           (article-mime-decode-quoted-printable 
526            (goto-char (point-min)) (point-max))
527           (subst-char-in-region (point-min) (point-max) ?_ ? )
528           (goto-char (point-max)))
529         (when (looking-at "\\([ \t\n]+\\)=\\?")
530           (replace-match "" t t nil 1))
531         (goto-char (point-min))))))
532
533 (defun article-de-quoted-unreadable (&optional force)
534   "Do a naive translation of a quoted-printable-encoded article.
535 This is in no way, shape or form meant as a replacement for real MIME
536 processing, but is simply a stop-gap measure until MIME support is
537 written.
538 If FORCE, decode the article whether it is marked as quoted-printable
539 or not."
540   (interactive (list 'force))
541   (save-excursion
542     (let ((case-fold-search t)
543           (buffer-read-only nil)
544           (type (gnus-fetch-field "content-transfer-encoding")))
545       (article-decode-rfc1522)
546       (when (or force
547                 (and type (string-match "quoted-printable" (downcase type))))
548         (goto-char (point-min))
549         (search-forward "\n\n" nil 'move)
550         (article-mime-decode-quoted-printable (point) (point-max))))))
551
552 (defun article-mime-decode-quoted-printable-buffer ()
553   "Decode Quoted-Printable in the current buffer."
554   (article-mime-decode-quoted-printable (point-min) (point-max)))
555   
556 (defun article-mime-decode-quoted-printable (from to)
557   "Decode Quoted-Printable in the region between FROM and TO."
558   (interactive "r")
559   (goto-char from)
560   (while (search-forward "=" to t)
561     (cond ((eq (following-char) ?\n)
562            (delete-char -1)
563            (delete-char 1))
564           ((looking-at "[0-9A-F][0-9A-F]")
565            (subst-char-in-region
566             (1- (point)) (point) ?=
567             (hexl-hex-string-to-integer
568              (buffer-substring (point) (+ 2 (point)))))
569            (delete-char 2))
570           ((looking-at "=")
571            (delete-char 1))
572           ((gnus-message 3 "Malformed MIME quoted-printable message")))))
573
574 (defun article-hide-pgp (&optional arg)
575   "Toggle hiding of any PGP headers and signatures in the current article.
576 If given a negative prefix, always show; if given a positive prefix,
577 always hide."
578   (interactive (article-hidden-arg))
579   (unless (article-check-hidden-text 'pgp arg)
580     (save-excursion
581       (let (buffer-read-only beg end)
582         (widen)
583         (goto-char (point-min))
584         ;; Hide the "header".
585         (and (search-forward "\n-----BEGIN PGP SIGNED MESSAGE-----\n" nil t)
586              (article-hide-text-type (match-beginning 0) (match-end 0) 'pgp))
587         (setq beg (point))
588         ;; Hide the actual signature.
589         (and (search-forward "\n-----BEGIN PGP SIGNATURE-----\n" nil t)
590              (setq end (1+ (match-beginning 0)))
591              (article-hide-text-type
592               end
593               (if (search-forward "\n-----END PGP SIGNATURE-----\n" nil t)
594                   (match-end 0)
595                 ;; Perhaps we shouldn't hide to the end of the buffer
596                 ;; if there is no end to the signature?
597                 (point-max))
598               'pgp))
599         ;; Hide "- " PGP quotation markers.
600         (when (and beg end)
601           (narrow-to-region beg end)
602           (goto-char (point-min))
603           (while (re-search-forward "^- " nil t)
604             (article-hide-text-type (match-beginning 0) (match-end 0) 'pgp))
605           (widen))))))
606
607 (defun article-hide-pem (&optional arg)
608   "Toggle hiding of any PEM headers and signatures in the current article.
609 If given a negative prefix, always show; if given a positive prefix,
610 always hide."
611   (interactive (article-hidden-arg))
612   (unless (article-check-hidden-text 'pem arg)
613     (save-excursion
614       (let (buffer-read-only end)
615         (widen)
616         (goto-char (point-min))
617         ;; hide the horrendously ugly "header".
618         (and (search-forward "\n-----BEGIN PRIVACY-ENHANCED MESSAGE-----\n"
619                              nil
620                              t)
621              (setq end (1+ (match-beginning 0)))
622              (article-hide-text-type
623               end
624               (if (search-forward "\n\n" nil t)
625                   (match-end 0)
626                 (point-max))
627               'pem))
628         ;; hide the trailer as well
629         (and (search-forward "\n-----END PRIVACY-ENHANCED MESSAGE-----\n"
630                              nil
631                              t)
632              (article-hide-text-type
633               (match-beginning 0) (match-end 0) 'pem))))))
634
635 (defun article-hide-signature (&optional arg)
636   "Hide the signature in the current article.
637 If given a negative prefix, always show; if given a positive prefix,
638 always hide."
639   (interactive (article-hidden-arg))
640   (unless (article-check-hidden-text 'signature arg)
641     (save-excursion
642       (save-restriction
643         (let ((buffer-read-only nil))
644           (when (article-narrow-to-signature)
645             (article-hide-text-type (point-min) (point-max) 'signature)))))))
646
647 (defun article-strip-leading-blank-lines ()
648   "Remove all blank lines from the beginning of the article."
649   (interactive)
650   (save-excursion
651     (let ((inhibit-point-motion-hooks t)
652           buffer-read-only)
653       (goto-char (point-min))
654       (when (search-forward "\n\n" nil t)
655         (while (and (not (eobp))
656                     (looking-at "[ \t]*$"))
657           (gnus-delete-line))))))
658
659 (defun article-strip-multiple-blank-lines ()
660   "Replace consecutive blank lines with one empty line."
661   (interactive)
662   (save-excursion
663     (let (buffer-read-only)
664       ;; First make all blank lines empty.
665       (goto-char (point-min))
666       (while (re-search-forward "^[ \t]+$" nil t)
667         (replace-match "" nil t))
668       ;; Then replace multiple empty lines with a single empty line.
669       (goto-char (point-min))
670       (while (re-search-forward "\n\n\n+" nil t)
671         (replace-match "\n\n" t t)))))
672
673 (defun article-strip-blank-lines ()
674   "Strip leading, trailing and multiple blank lines."
675   (interactive)
676   (article-strip-leading-blank-lines)
677   (article-remove-trailing-blank-lines)
678   (article-strip-multiple-blank-lines))
679
680 (defvar mime::preview/content-list)
681 (defvar mime::preview-content-info/point-min)
682 (defun article-narrow-to-signature ()
683   "Narrow to the signature; return t if a signature is found, else nil."
684   (widen)
685   (when (and (boundp 'mime::preview/content-list)
686              mime::preview/content-list)
687     ;; We have a MIMEish article, so we use the MIME data to narrow.
688     (let ((pcinfo (car (last mime::preview/content-list))))
689       (ignore-errors
690         (narrow-to-region
691          (funcall (intern "mime::preview-content-info/point-min") pcinfo)
692          (point-max)))))
693   
694   (when (article-search-signature)
695     (forward-line 1)
696     ;; Check whether we have some limits to what we consider
697     ;; to be a signature.
698     (let ((limits (if (listp gnus-signature-limit) gnus-signature-limit
699                     (list gnus-signature-limit)))
700           limit limited)
701       (while (setq limit (pop limits))
702         (if (or (and (integerp limit)
703                      (< (- (point-max) (point)) limit))
704                 (and (floatp limit)
705                      (< (count-lines (point) (point-max)) limit))
706                 (and (gnus-functionp limit)
707                      (funcall limit))
708                 (and (stringp limit)
709                      (not (re-search-forward limit nil t))))
710             ()                          ; This limit did not succeed.
711           (setq limited t
712                 limits nil)))
713       (unless limited
714         (narrow-to-region (point) (point-max))
715         t))))
716
717 (defun article-search-signature ()
718   "Search the current buffer for the signature separator.
719 Put point at the beginning of the signature separator."
720   (let ((cur (point)))
721     (goto-char (point-max))
722     (if (if (stringp gnus-signature-separator)
723             (re-search-backward gnus-signature-separator nil t)
724           (let ((seps gnus-signature-separator))
725             (while (and seps
726                         (not (re-search-backward (car seps) nil t)))
727               (pop seps))
728             seps))
729         t
730       (goto-char cur)
731       nil)))
732
733 (defun article-hidden-arg ()
734   "Return the current prefix arg as a number, or 0 if no prefix."
735   (list (if current-prefix-arg
736             (prefix-numeric-value current-prefix-arg)
737           0)))
738
739 (defun article-check-hidden-text (type arg)
740   "Return nil if hiding is necessary.
741 Arg can be nil or a number.  Nil and positive means hide, negative
742 means show, 0 means toggle."
743   (save-excursion
744     (let ((hide (article-hidden-text-p type)))
745       (cond
746        ((or (null arg)
747             (> arg 0))
748         nil)
749        ((< arg 0)
750         (article-show-hidden-text type))
751        (t
752         (if (eq hide 'hidden)
753             (article-show-hidden-text type)
754           nil))))))
755
756 (defun article-hidden-text-p (type)
757   "Say whether the current buffer contains hidden text of type TYPE."
758   (let ((pos (text-property-any (point-min) (point-max) 'article-type type)))
759     (when pos
760       (if (get-text-property pos 'invisible)
761           'hidden
762         'shown))))
763
764 (defun article-show-hidden-text (type &optional hide)
765   "Show all hidden text of type TYPE.
766 If HIDE, hide the text instead."
767   (save-excursion
768     (let ((buffer-read-only nil)
769           (inhibit-point-motion-hooks t)
770           (beg (point-min)))
771       (while (gnus-goto-char (text-property-any
772                               beg (point-max) 'article-type type))
773         (setq beg (point))
774         (forward-char)
775         (if hide
776             (article-hide-text beg (point) gnus-hidden-properties)
777           (article-unhide-text beg (point)))
778         (setq beg (point)))
779       t)))
780
781 (defconst article-time-units
782   `((year . ,(* 365.25 24 60 60))
783     (week . ,(* 7 24 60 60))
784     (day . ,(* 24 60 60))
785     (hour . ,(* 60 60))
786     (minute . 60)
787     (second . 1))
788   "Mapping from time units to seconds.")
789
790 (defun article-date-ut (&optional type highlight header)
791   "Convert DATE date to universal time in the current article.
792 If TYPE is `local', convert to local time; if it is `lapsed', output
793 how much time has lapsed since DATE."
794   (interactive (list 'ut t))
795   (let* ((header (or header (message-fetch-field "date") ""))
796          (date (if (vectorp header) (mail-header-date header)
797                  header))
798          (date-regexp "^Date: \\|^X-Sent: ")
799          (inhibit-point-motion-hooks t)
800          bface eface)
801     (when (and date (not (string= date "")))
802       (save-excursion
803         (save-restriction
804           (nnheader-narrow-to-headers)
805           (let ((buffer-read-only nil))
806             ;; Delete any old Date headers.
807             (if (re-search-forward date-regexp nil t)
808                 (progn
809                   (setq bface (get-text-property (gnus-point-at-bol) 'face)
810                         eface (get-text-property (1- (gnus-point-at-eol))
811                                                  'face))
812                   (message-remove-header date-regexp t)
813                   (beginning-of-line))
814               (goto-char (point-max)))
815             (insert (article-make-date-line date type))
816             ;; Do highlighting.
817             (forward-line -1)
818             (when (looking-at "\\([^:]+\\): *\\(.*\\)$")
819               (put-text-property (match-beginning 1) (match-end 1)
820                                  'face bface)
821               (put-text-property (match-beginning 2) (match-end 2)
822                                  'face eface))))))))
823
824 (defun article-make-date-line (date type)
825   "Return a DATE line of TYPE."
826   (cond
827    ;; Convert to the local timezone.  We have to slap a
828    ;; `condition-case' round the calls to the timezone
829    ;; functions since they aren't particularly resistant to
830    ;; buggy dates.
831    ((eq type 'local)
832     (concat "Date: " (condition-case ()
833                          (timezone-make-date-arpa-standard date)
834                        (error date))
835             "\n"))
836    ;; Convert to Universal Time.
837    ((eq type 'ut)
838     (concat "Date: "
839             (condition-case ()
840                 (timezone-make-date-arpa-standard date nil "UT")
841               (error date))
842             "\n"))
843    ;; Get the original date from the article.
844    ((eq type 'original)
845     (concat "Date: " date "\n"))
846    ;; Do an X-Sent lapsed format.
847    ((eq type 'lapsed)
848     ;; If the date is seriously mangled, the timezone functions are
849     ;; liable to bug out, so we ignore all errors.
850     (let* ((now (current-time))
851            (real-time
852             (ignore-errors
853               (gnus-time-minus
854                (gnus-encode-date
855                 (timezone-make-date-arpa-standard
856                  (current-time-string now)
857                  (current-time-zone now) "UT"))
858                (gnus-encode-date
859                 (timezone-make-date-arpa-standard
860                  date nil "UT")))))
861            (real-sec (and real-time
862                           (+ (* (float (car real-time)) 65536)
863                              (cadr real-time))))
864            (sec (and real-time (abs real-sec)))
865            num prev)
866       (cond
867        ((null real-time)
868         "X-Sent: Unknown\n")
869        ((zerop sec)
870         "X-Sent: Now\n")
871        (t
872         (concat
873          "X-Sent: "
874          ;; This is a bit convoluted, but basically we go
875          ;; through the time units for years, weeks, etc,
876          ;; and divide things to see whether that results
877          ;; in positive answers.
878          (mapconcat
879           (lambda (unit)
880             (if (zerop (setq num (ffloor (/ sec (cdr unit)))))
881                 ;; The (remaining) seconds are too few to
882                 ;; be divided into this time unit.
883                 ""
884               ;; It's big enough, so we output it.
885               (setq sec (- sec (* num (cdr unit))))
886               (prog1
887                   (concat (if prev ", " "") (int-to-string
888                                              (floor num))
889                           " " (symbol-name (car unit)) 
890                           (if (> num 1) "s" ""))
891                 (setq prev t))))
892           article-time-units "")
893          ;; If dates are odd, then it might appear like the
894          ;; article was sent in the future.
895          (if (> real-sec 0)
896              " ago\n"
897            " in the future\n"))))))
898    (t
899     (error "Unknown conversion type: %s" type))))
900
901 (defun article-date-local (&optional highlight)
902   "Convert the current article date to the local timezone."
903   (interactive (list t))
904   (article-date-ut 'local highlight))
905
906 (defun article-date-original (&optional highlight)
907   "Convert the current article date to what it was originally.
908 This is only useful if you have used some other date conversion
909 function and want to see what the date was before converting."
910   (interactive (list t))
911   (article-date-ut 'original highlight))
912
913 (defun article-date-lapsed (&optional highlight)
914   "Convert the current article date to time lapsed since it was sent."
915   (interactive (list t))
916   (article-date-ut 'lapsed highlight))
917
918 (defun article-show-all ()
919   "Show all hidden text in the article buffer."
920   (interactive)
921   (save-excursion
922     (let ((buffer-read-only nil))
923       (article-unhide-text (point-min) (point-max)))))
924
925 (defun article-emphasize (&optional arg)
926   "Emphasize text according to `gnus-emphasis-alist'."
927   (interactive (article-hidden-arg))
928   (unless (article-check-hidden-text 'emphasis arg)
929     (save-excursion
930       (let ((alist gnus-emphasis-alist)
931             (buffer-read-only nil)
932             (props (append '(article-type emphasis)
933                            gnus-hidden-properties))
934             regexp elem beg invisible visible face)
935         (goto-char (point-min))
936         (search-forward "\n\n" nil t)
937         (setq beg (point))
938         (while (setq elem (pop alist))
939           (goto-char beg)
940           (setq regexp (car elem)
941                 invisible (nth 1 elem)
942                 visible (nth 2 elem)
943                 face (nth 3 elem))
944           (while (re-search-forward regexp nil t)
945             (when (and (match-beginning visible) (match-beginning invisible))
946               (article-hide-text
947                (match-beginning invisible) (match-end invisible) props)
948               (article-unhide-text-type
949                (match-beginning visible) (match-end visible) 'emphasis)
950               (put-text-property 
951                (match-beginning visible) (match-end visible) 'face face)
952               (goto-char (match-end invisible)))))))))
953
954 (provide 'article)
955
956 ;;; article.el ends here