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