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