*** empty log message ***
[gnus] / lisp / nnweb.el
1 ;;; nnweb.el --- retrieving articles via web search engines
2 ;; Copyright (C) 1996,97,98,99 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
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 ;; Note: You need to have `url' and `w3' installed for this
27 ;; backend to work.
28
29 ;;; Code:
30
31 (eval-when-compile (require 'cl))
32
33 (require 'nnoo)
34 (require 'message)
35 (require 'gnus-util)
36 (require 'gnus)
37 (require 'nnmail)
38 (require 'mm-util)
39 (eval-when-compile
40   (ignore-errors
41     (require 'w3)
42     (require 'url)
43     (require 'w3-forms)))
44 ;; Report failure to find w3 at load time if appropriate.
45 (eval '(progn
46          (require 'w3)
47          (require 'url)
48          (require 'w3-forms)))
49
50 (nnoo-declare nnweb)
51
52 (defvoo nnweb-directory (nnheader-concat gnus-directory "nnweb/")
53   "Where nnweb will save its files.")
54
55 (defvoo nnweb-type 'dejanews
56   "What search engine type is being used.
57 Valid types include `dejanews', `dejanewsold', `reference',
58 and `altavista'.")
59
60 (defvoo nnweb-type-definition
61   '((dejanews
62      (article . nnweb-dejanews-wash-article)
63      (map . nnweb-dejanews-create-mapping)
64      (search . nnweb-dejanews-search)
65      (address . "http://x8.dejanews.com/dnquery.xp")
66      (identifier . nnweb-dejanews-identity))
67     (dejanewsold
68      (article . nnweb-dejanews-wash-article)
69      (map . nnweb-dejanews-create-mapping)
70      (search . nnweb-dejanewsold-search)
71      (address . "http://x8.dejanews.com/dnquery.xp")
72      (identifier . nnweb-dejanews-identity))
73     (reference
74      (article . nnweb-reference-wash-article)
75      (map . nnweb-reference-create-mapping)
76      (search . nnweb-reference-search)
77      (address . "http://www.reference.com/cgi-bin/pn/go")
78      (identifier . identity))
79     (altavista
80      (article . nnweb-altavista-wash-article)
81      (map . nnweb-altavista-create-mapping)
82      (search . nnweb-altavista-search)
83      (address . "http://www.altavista.digital.com/cgi-bin/query")
84      (id . "/cgi-bin/news?id@%s")
85      (identifier . identity)))
86   "Type-definition alist.")
87
88 (defvoo nnweb-search nil
89   "Search string to feed to DejaNews.")
90
91 (defvoo nnweb-max-hits 999
92   "Maximum number of hits to display.")
93
94 (defvoo nnweb-ephemeral-p nil
95   "Whether this nnweb server is ephemeral.")
96
97 ;;; Internal variables
98
99 (defvoo nnweb-articles nil)
100 (defvoo nnweb-buffer nil)
101 (defvoo nnweb-group-alist nil)
102 (defvoo nnweb-group nil)
103 (defvoo nnweb-hashtb nil)
104
105 ;;; Interface functions
106
107 (nnoo-define-basics nnweb)
108
109 (deffoo nnweb-retrieve-headers (articles &optional group server fetch-old)
110   (nnweb-possibly-change-server group server)
111   (save-excursion
112     (set-buffer nntp-server-buffer)
113     (erase-buffer)
114     (let (article header)
115       (while (setq article (pop articles))
116         (when (setq header (cadr (assq article nnweb-articles)))
117           (nnheader-insert-nov header)))
118       'nov)))
119
120 (deffoo nnweb-request-scan (&optional group server)
121   (nnweb-possibly-change-server group server)
122   (setq nnweb-hashtb (gnus-make-hashtable 4095))
123   (funcall (nnweb-definition 'map))
124   (unless nnweb-ephemeral-p
125     (nnweb-write-active)
126     (nnweb-write-overview group)))
127
128 (deffoo nnweb-request-group (group &optional server dont-check)
129   (nnweb-possibly-change-server nil server)
130   (when (and group
131              (not (equal group nnweb-group))
132              (not nnweb-ephemeral-p))
133     (let ((info (assoc group nnweb-group-alist)))
134       (setq nnweb-group group)
135       (setq nnweb-type (nth 2 info))
136       (setq nnweb-search (nth 3 info))
137       (unless dont-check
138         (nnweb-read-overview group))))
139   (cond
140    ((not nnweb-articles)
141     (nnheader-report 'nnweb "No matching articles"))
142    (t
143     (let ((active (if nnweb-ephemeral-p
144                       (cons (caar nnweb-articles)
145                             (caar (last nnweb-articles)))
146                     (cadr (assoc group nnweb-group-alist)))))
147       (nnheader-report 'nnweb "Opened group %s" group)
148       (nnheader-insert
149        "211 %d %d %d %s\n" (length nnweb-articles)
150        (car active) (cdr active) group)))))
151
152 (deffoo nnweb-close-group (group &optional server)
153   (nnweb-possibly-change-server group server)
154   (when (gnus-buffer-live-p nnweb-buffer)
155     (save-excursion
156       (set-buffer nnweb-buffer)
157       (set-buffer-modified-p nil)
158       (kill-buffer nnweb-buffer)))
159   t)
160
161 (deffoo nnweb-request-article (article &optional group server buffer)
162   (nnweb-possibly-change-server group server)
163   (save-excursion
164     (set-buffer (or buffer nntp-server-buffer))
165     (let* ((header (cadr (assq article nnweb-articles)))
166            (url (and header (mail-header-xref header))))
167       (when (or (and url
168                      (nnweb-fetch-url url))
169                 (and (stringp article)
170                      (nnweb-definition 'id t)
171                      (let ((fetch (nnweb-definition 'id))
172                            art)
173                        (when (string-match "^<\\(.*\\)>$" article)
174                          (setq art (match-string 1 article)))
175                        (and fetch
176                             art
177                             (nnweb-fetch-url
178                              (format fetch article))))))
179         (unless nnheader-callback-function
180           (funcall (nnweb-definition 'article))
181           (nnweb-decode-entities))
182         (nnheader-report 'nnweb "Fetched article %s" article)
183         t))))
184
185 (deffoo nnweb-close-server (&optional server)
186   (when (and (nnweb-server-opened server)
187              (gnus-buffer-live-p nnweb-buffer))
188     (save-excursion
189       (set-buffer nnweb-buffer)
190       (set-buffer-modified-p nil)
191       (kill-buffer nnweb-buffer)))
192   (nnoo-close-server 'nnweb server))
193
194 (deffoo nnweb-request-list (&optional server)
195   (nnweb-possibly-change-server nil server)
196   (save-excursion
197     (set-buffer nntp-server-buffer)
198     (nnmail-generate-active nnweb-group-alist)
199     t))
200
201 (deffoo nnweb-request-update-info (group info &optional server)
202   (nnweb-possibly-change-server group server)
203   ;;(setcar (cddr info) nil)
204   )
205
206 (deffoo nnweb-asynchronous-p ()
207   t)
208
209 (deffoo nnweb-request-create-group (group &optional server args)
210   (nnweb-possibly-change-server nil server)
211   (nnweb-request-delete-group group)
212   (push `(,group ,(cons 1 0) ,@args) nnweb-group-alist)
213   (nnweb-write-active)
214   t)
215
216 (deffoo nnweb-request-delete-group (group &optional force server)
217   (nnweb-possibly-change-server group server)
218   (gnus-pull group nnweb-group-alist t)
219   (nnweb-write-active)
220   (gnus-delete-file (nnweb-overview-file group))
221   t)
222
223 (nnoo-define-skeleton nnweb)
224
225 ;;; Internal functions
226
227 (defun nnweb-read-overview (group)
228   "Read the overview of GROUP and build the map."
229   (when (file-exists-p (nnweb-overview-file group))
230     (with-temp-buffer
231       (nnheader-insert-file-contents (nnweb-overview-file group))
232       (goto-char (point-min))
233       (let (header)
234         (while (not (eobp))
235           (setq header (nnheader-parse-nov))
236           (forward-line 1)
237           (push (list (mail-header-number header)
238                       header (mail-header-xref header))
239                 nnweb-articles)
240           (nnweb-set-hashtb header (car nnweb-articles)))))))
241
242 (defun nnweb-write-overview (group)
243   "Write the overview file for GROUP."
244   (with-temp-file (nnweb-overview-file group)
245     (let ((articles nnweb-articles))
246       (while articles
247         (nnheader-insert-nov (cadr (pop articles)))))))
248
249 (defun nnweb-set-hashtb (header data)
250   (gnus-sethash (nnweb-identifier (mail-header-xref header))
251                 data nnweb-hashtb))
252
253 (defun nnweb-get-hashtb (url)
254   (gnus-gethash (nnweb-identifier url) nnweb-hashtb))
255
256 (defun nnweb-identifier (ident)
257   (funcall (nnweb-definition 'identifier) ident))
258
259 (defun nnweb-overview-file (group)
260   "Return the name of the overview file of GROUP."
261   (nnheader-concat nnweb-directory group ".overview"))
262
263 (defun nnweb-write-active ()
264   "Save the active file."
265   (with-temp-file (nnheader-concat nnweb-directory "active")
266     (prin1 `(setq nnweb-group-alist ',nnweb-group-alist) (current-buffer))))
267
268 (defun nnweb-read-active ()
269   "Read the active file."
270   (load (nnheader-concat nnweb-directory "active") t t t))
271
272 (defun nnweb-definition (type &optional noerror)
273   "Return the definition of TYPE."
274   (let ((def (cdr (assq type (assq nnweb-type nnweb-type-definition)))))
275     (when (and (not def)
276                (not noerror))
277       (error "Undefined definition %s" type))
278     def))
279
280 (defun nnweb-possibly-change-server (&optional group server)
281   (nnweb-init server)
282   (when server
283     (unless (nnweb-server-opened server)
284       (nnweb-open-server server)))
285   (unless nnweb-group-alist
286     (nnweb-read-active))
287   (when group
288     (when (and (not nnweb-ephemeral-p)
289                (not (equal group nnweb-group)))
290       (nnweb-request-group group nil t))))
291
292 (defun nnweb-init (server)
293   "Initialize buffers and such."
294   (unless (gnus-buffer-live-p nnweb-buffer)
295     (setq nnweb-buffer
296           (save-excursion
297             (nnheader-set-temp-buffer
298              (format " *nnweb %s %s %s*" nnweb-type nnweb-search server))))))
299
300 (defun nnweb-fetch-url (url)
301   (save-excursion
302     (if (not nnheader-callback-function)
303         (let ((buf (current-buffer)))
304           (save-excursion
305             (set-buffer nnweb-buffer)
306             (erase-buffer)
307             (url-insert-file-contents url)
308             (copy-to-buffer buf (point-min) (point-max))
309             t))
310       (nnweb-url-retrieve-asynch
311        url 'nnweb-callback (current-buffer) nnheader-callback-function)
312       t)))
313
314 (defun nnweb-callback (buffer callback)
315   (when (gnus-buffer-live-p url-working-buffer)
316     (save-excursion
317       (set-buffer url-working-buffer)
318       (funcall (nnweb-definition 'article))
319       (nnweb-decode-entities)
320       (set-buffer buffer)
321       (goto-char (point-max))
322       (insert-buffer-substring url-working-buffer))
323     (funcall callback t)
324     (gnus-kill-buffer url-working-buffer)))
325
326 (defun nnweb-url-retrieve-asynch (url callback &rest data)
327   (let ((url-request-method "GET")
328         (old-asynch url-be-asynchronous)
329         (url-request-data nil)
330         (url-request-extra-headers nil)
331         (url-working-buffer (generate-new-buffer-name " *nnweb*")))
332     (setq-default url-be-asynchronous t)
333     (save-excursion
334       (set-buffer (get-buffer-create url-working-buffer))
335       (setq url-current-callback-data data
336             url-be-asynchronous t
337             url-current-callback-func callback)
338       (url-retrieve url))
339     (setq-default url-be-asynchronous old-asynch)))
340
341 ;;;
342 ;;; DejaNews functions.
343 ;;;
344
345 (defun nnweb-dejanews-create-mapping ()
346   "Perform the search and create an number-to-url alist."
347   (save-excursion
348     (set-buffer nnweb-buffer)
349     (erase-buffer)
350     (when (funcall (nnweb-definition 'search) nnweb-search)
351       (let ((i 0)
352             (more t)
353             (case-fold-search t)
354             (active (or (cadr (assoc nnweb-group nnweb-group-alist))
355                         (cons 1 0)))
356             Subject (Score "0") Date Newsgroup Author
357             map url)
358         (while more
359           ;; Go through all the article hits on this page.
360           (goto-char (point-min))
361           (nnweb-decode-entities)
362           (goto-char (point-min))
363           (while (re-search-forward "^ <P>\n" nil t)
364             (narrow-to-region
365              (point)
366              (cond ((re-search-forward "^ <P>\n" nil t)
367                     (match-beginning 0))
368                    ((search-forward "\n\n" nil t)
369                     (point))
370                    (t
371                     (point-max))))
372             (goto-char (point-min))
373             (looking-at ".*HREF=\"\\([^\"]+\\)\"\\(.*\\)")
374             (setq url (match-string 1))
375             (let ((begin (point)))
376               (nnweb-remove-markup)
377               (goto-char begin)
378               (while (search-forward "\t" nil t)
379                 (replace-match " "))
380               (goto-char begin)
381               (end-of-line)
382               (setq Subject (buffer-substring begin (point)))
383               (if (re-search-forward
384                    "^ Newsgroup: \\(.*\\)\n Posted on \\([0-9/]+\\) by \\(.*\\)$" nil t)
385                   (setq Newsgroup (match-string 1)
386                         Date (match-string 2)
387                         Author (match-string 3))))
388             (widen)
389             (incf i)
390             (unless (nnweb-get-hashtb url)
391               (push
392                (list
393                 (incf (cdr active))
394                 (make-full-mail-header
395                  (cdr active) Subject Author Date
396                  (concat "<" (nnweb-identifier url) "@dejanews>")
397                  nil 0 (string-to-int Score) url))
398                map)
399               (nnweb-set-hashtb (cadar map) (car map))))
400           ;; See whether there is a "Get next 20 hits" button here.
401           (if (or (not (re-search-forward
402                         "HREF=\"\\([^\"]+\\)\"[<>b]+Next result" nil t))
403                   (>= i nnweb-max-hits))
404               (setq more nil)
405             ;; Yup -- fetch it.
406             (setq more (match-string 1))
407             (erase-buffer)
408             (url-insert-file-contents more)))
409         ;; Return the articles in the right order.
410         (setq nnweb-articles
411               (sort (nconc nnweb-articles map) 'car-less-than-car))))))
412
413 (defun nnweb-dejanews-wash-article ()
414   (let ((case-fold-search t))
415     (goto-char (point-min))
416     (re-search-forward "<PRE>" nil t)
417     (delete-region (point-min) (point))
418     (re-search-forward "</PRE>" nil t)
419     (delete-region (point) (point-max))
420     (nnweb-remove-markup)
421     (goto-char (point-min))
422     (while (and (looking-at " *$")
423                 (not (eobp)))
424       (gnus-delete-line))
425     (while (looking-at "\\(^[^ ]+:\\) *")
426       (replace-match "\\1 " t)
427       (forward-line 1))
428     (when (re-search-forward "\n\n+" nil t)
429       (replace-match "\n" t t))
430     (goto-char (point-min))
431     (when (search-forward "[More Headers]" nil t)
432       (replace-match "" t t))))
433
434 (defun nnweb-dejanews-search (search)
435   (nnweb-fetch-form
436    (nnweb-definition 'address)
437    `(("query" . ,search)
438      ("defaultOp" . "AND")
439      ("svcclass" . "dncurrent")
440      ("maxhits" . "100")
441      ("format" . "verbose2")
442      ("threaded" . "0")
443      ("showsort" . "date")
444      ("agesign" . "1")
445      ("ageweight" . "1")))
446   t)
447
448 (defun nnweb-dejanewsold-search (search)
449   (nnweb-fetch-form
450    (nnweb-definition 'address)
451    `(("query" . ,search)
452      ("defaultOp" . "AND")
453      ("svcclass" . "dnold")
454      ("maxhits" . "100")
455      ("format" . "verbose2")
456      ("threaded" . "0")
457      ("showsort" . "date")
458      ("agesign" . "1")
459      ("ageweight" . "1")))
460   t)
461
462 (defun nnweb-dejanews-identity (url)
463   "Return an unique identifier based on URL."
464   (if (string-match "recnum=\\([0-9]+\\)" url)
465       (match-string 1 url)
466     url))
467
468 ;;;
469 ;;; InReference
470 ;;;
471
472 (defun nnweb-reference-create-mapping ()
473   "Perform the search and create an number-to-url alist."
474   (save-excursion
475     (set-buffer nnweb-buffer)
476     (erase-buffer)
477     (when (funcall (nnweb-definition 'search) nnweb-search)
478       (let ((i 0)
479             (more t)
480             (case-fold-search t)
481             (active (or (cadr (assoc nnweb-group nnweb-group-alist))
482                         (cons 1 0)))
483             Subject Score Date Newsgroups From Message-ID
484             map url)
485         (while more
486           ;; Go through all the article hits on this page.
487           (goto-char (point-min))
488           (search-forward "</pre><hr>" nil t)
489           (delete-region (point-min) (point))
490                                         ;(nnweb-decode-entities)
491           (goto-char (point-min))
492           (while (re-search-forward "^ +[0-9]+\\." nil t)
493             (narrow-to-region
494              (point)
495              (if (re-search-forward "^$" nil t)
496                  (match-beginning 0)
497                (point-max)))
498             (goto-char (point-min))
499             (when (looking-at ".*href=\"\\([^\"]+\\)\"")
500               (setq url (match-string 1)))
501             (nnweb-remove-markup)
502             (goto-char (point-min))
503             (while (search-forward "\t" nil t)
504               (replace-match " "))
505             (goto-char (point-min))
506             (while (re-search-forward "^\\([^:]+\\): \\(.*\\)$" nil t)
507               (set (intern (match-string 1)) (match-string 2)))
508             (widen)
509             (search-forward "</pre>" nil t)
510             (incf i)
511             (unless (nnweb-get-hashtb url)
512               (push
513                (list
514                 (incf (cdr active))
515                 (make-full-mail-header
516                  (cdr active) (concat  "(" Newsgroups ") " Subject) From Date
517                  Message-ID
518                  nil 0 (string-to-int Score) url))
519                map)
520               (nnweb-set-hashtb (cadar map) (car map))))
521           (setq more nil))
522         ;; Return the articles in the right order.
523         (setq nnweb-articles
524               (sort (nconc nnweb-articles map) 'car-less-than-car))))))
525
526 (defun nnweb-reference-wash-article ()
527   (let ((case-fold-search t))
528     (goto-char (point-min))
529     (re-search-forward "^</center><hr>" nil t)
530     (delete-region (point-min) (point))
531     (search-forward "<pre>" nil t)
532     (forward-line -1)
533     (let ((body (point-marker)))
534       (search-forward "</pre>" nil t)
535       (delete-region (point) (point-max))
536       (nnweb-remove-markup)
537       (goto-char (point-min))
538       (while (looking-at " *$")
539         (gnus-delete-line))
540       (narrow-to-region (point-min) body)
541       (while (and (re-search-forward "^$" nil t)
542                   (not (eobp)))
543         (gnus-delete-line))
544       (goto-char (point-min))
545       (while (looking-at "\\(^[^ ]+:\\) *")
546         (replace-match "\\1 " t)
547         (forward-line 1))
548       (goto-char (point-min))
549       (when (re-search-forward "^References:" nil t)
550         (narrow-to-region
551          (point) (if (re-search-forward "^$\\|^[^:]+:" nil t)
552                      (match-beginning 0)
553                    (point-max)))
554         (goto-char (point-min))
555         (while (not (eobp))
556           (unless (looking-at "References")
557             (insert "\t")
558             (forward-line 1)))
559         (goto-char (point-min))
560         (while (search-forward "," nil t)
561           (replace-match " " t t)))
562       (widen)
563       (set-marker body nil))))
564
565 (defun nnweb-reference-search (search)
566   (url-insert-file-contents
567    (concat
568     (nnweb-definition 'address)
569     "?"
570     (nnweb-encode-www-form-urlencoded
571      `(("search" . "advanced")
572        ("querytext" . ,search)
573        ("subj" . "")
574        ("name" . "")
575        ("login" . "")
576        ("host" . "")
577        ("organization" . "")
578        ("groups" . "")
579        ("keywords" . "")
580        ("choice" . "Search")
581        ("startmonth" . "Jul")
582        ("startday" . "25")
583        ("startyear" . "1996")
584        ("endmonth" . "Aug")
585        ("endday" . "24")
586        ("endyear" . "1996")
587        ("mode" . "Quick")
588        ("verbosity" . "Verbose")
589        ("ranking" . "Relevance")
590        ("first" . "1")
591        ("last" . "25")
592        ("score" . "50")))))
593   (setq buffer-file-name nil)
594   t)
595
596 ;;;
597 ;;; Alta Vista
598 ;;;
599
600 (defun nnweb-altavista-create-mapping ()
601   "Perform the search and create an number-to-url alist."
602   (save-excursion
603     (set-buffer nnweb-buffer)
604     (erase-buffer)
605     (let ((part 0))
606       (when (funcall (nnweb-definition 'search) nnweb-search part)
607         (let ((i 0)
608               (more t)
609               (case-fold-search t)
610               (active (or (cadr (assoc nnweb-group nnweb-group-alist))
611                           (cons 1 0)))
612               subject date from id group
613               map url)
614           (while more
615             ;; Go through all the article hits on this page.
616             (goto-char (point-min))
617             (search-forward "<dt>" nil t)
618             (delete-region (point-min) (match-beginning 0))
619             (goto-char (point-min))
620             (while (search-forward "<dt>" nil t)
621               (replace-match "\n<blubb>"))
622             (nnweb-decode-entities)
623             (goto-char (point-min))
624             (while (re-search-forward "<blubb>.*href=\"\\([^\"]+\\)\"><strong>\\([^>]*\\)</strong></a><dd>\\([^-]+\\)- <b>\\([^<]+\\)<.*href=\"news:\\([^\"]+\\)\">.*\">\\(.+\\)</a><P>"
625                                       nil t)
626               (setq url (match-string 1)
627                     subject (match-string 2)
628                     date (match-string 3)
629                     group (match-string 4)
630                     id (concat "<" (match-string 5) ">")
631                     from (match-string 6))
632               (incf i)
633               (unless (nnweb-get-hashtb url)
634                 (push
635                  (list
636                   (incf (cdr active))
637                   (make-full-mail-header
638                    (cdr active) (concat  "(" group ") " subject) from date
639                    id nil 0 0 url))
640                  map)
641                 (nnweb-set-hashtb (cadar map) (car map))))
642             ;; See if we want more.
643             (when (or (not nnweb-articles)
644                       (>= i nnweb-max-hits)
645                       (not (funcall (nnweb-definition 'search)
646                                     nnweb-search (incf part))))
647               (setq more nil)))
648           ;; Return the articles in the right order.
649           (setq nnweb-articles
650                 (sort (nconc nnweb-articles map) 'car-less-than-car)))))))
651
652 (defun nnweb-altavista-wash-article ()
653   (goto-char (point-min))
654   (let ((case-fold-search t))
655     (when (re-search-forward "^<strong>" nil t)
656       (delete-region (point-min) (match-beginning 0)))
657     (goto-char (point-min))
658     (while (looking-at "<strong>\\([^ ]+\\) +</strong> +\\(.*\\)$")
659       (replace-match "\\1: \\2" t)
660       (forward-line 1))
661     (when (re-search-backward "^References:" nil t)
662       (narrow-to-region (point) (progn (forward-line 1) (point)))
663       (goto-char (point-min))
664       (while (re-search-forward "<A.*\\?id@\\([^\"]+\\)\">[0-9]+</A>" nil t)
665         (replace-match "&lt;\\1&gt; " t)))
666     (widen)
667     (nnweb-remove-markup)))
668
669 (defun nnweb-altavista-search (search &optional part)
670   (url-insert-file-contents
671    (concat
672     (nnweb-definition 'address)
673     "?"
674     (nnweb-encode-www-form-urlencoded
675      `(("pg" . "aq")
676        ("what" . "news")
677        ,@(when part `(("stq" . ,(int-to-string (* part 30)))))
678        ("fmt" . "d")
679        ("q" . ,search)
680        ("r" . "")
681        ("d0" . "")
682        ("d1" . "")))))
683   (setq buffer-file-name nil)
684   t)
685
686 ;;;
687 ;;; General web/w3 interface utility functions
688 ;;;
689
690 (defun nnweb-insert-html (parse)
691   "Insert HTML based on a w3 parse tree."
692   (if (stringp parse)
693       (insert parse)
694     (insert "<" (symbol-name (car parse)) " ")
695     (insert (mapconcat
696              (lambda (param)
697                (concat (symbol-name (car param)) "="
698                        (prin1-to-string
699                         (if (consp (cdr param))
700                             (cadr param)
701                           (cdr param)))))
702              (nth 1 parse)
703              " "))
704     (insert ">\n")
705     (mapcar 'nnweb-insert-html (nth 2 parse))
706     (insert "</" (symbol-name (car parse)) ">\n")))
707
708 (defun nnweb-encode-www-form-urlencoded (pairs)
709   "Return PAIRS encoded for forms."
710   (mapconcat
711    (function
712     (lambda (data)
713       (concat (w3-form-encode-xwfu (car data)) "="
714               (w3-form-encode-xwfu (cdr data)))))
715    pairs "&"))
716
717 (defun nnweb-fetch-form (url pairs)
718   "Fetch a form from URL with PAIRS as the data."
719   (let ((url-request-data (nnweb-encode-www-form-urlencoded pairs))
720         (url-request-method "POST")
721         (url-request-extra-headers
722          '(("Content-type" . "application/x-www-form-urlencoded"))))
723     (url-insert-file-contents url)
724     (setq buffer-file-name nil))
725   t)
726
727 (defun nnweb-decode-entities ()
728   "Decode all HTML entities."
729   (goto-char (point-min))
730   (while (re-search-forward "&\\([a-z]+\\);" nil t)
731     (replace-match (char-to-string (or (cdr (assq (intern (match-string 1))
732                                                   w3-html-entities))
733                                        ?#))
734                    t t)))
735
736 (defun nnweb-remove-markup ()
737   "Remove all HTML markup, leaving just plain text."
738   (goto-char (point-min))
739   (while (search-forward "<!--" nil t)
740     (delete-region (match-beginning 0)
741                    (or (search-forward "-->" nil t)
742                        (point-max))))
743   (goto-char (point-min))
744   (while (re-search-forward "<[^>]+>" nil t)
745     (replace-match "" t t)))
746
747 (defun nnweb-insert (url)
748   "Insert the contents from an URL in the current buffer."
749   (let ((name buffer-file-name))
750     (url-insert-file-contents url)
751     (setq buffer-file-name name)))
752
753 (defun nnweb-parse-find (type parse &optional maxdepth)
754   "Find the element of TYPE in PARSE."
755   (catch 'found
756     (nnweb-parse-find-1 type parse maxdepth)))
757
758 (defun nnweb-parse-find-1 (type contents maxdepth)
759   (when (or (null maxdepth)
760             (not (zerop maxdepth)))
761     (when (consp contents)
762       (when (eq (car contents) type)
763         (throw 'found contents))
764       (when (listp (cdr contents))
765         (dolist (element contents)
766           (when (consp element)
767             (nnweb-parse-find-1 type element
768                                 (and maxdepth (1- maxdepth)))))))))
769
770 (defvar nnweb-text)
771 (defun nnweb-text (parse)
772   "Return a list of text contents in PARSE."
773   (let ((nnweb-text nil))
774     (nnweb-text-1 parse)
775     (nreverse nnweb-text)))
776
777 (defun nnweb-text-1 (contents)
778   (dolist (element contents)
779     (if (stringp element)
780         (push element nnweb-text)
781       (when (and (consp element)
782                  (listp (cdr element)))
783         (nnweb-text-1 element)))))
784
785 (provide 'nnweb)
786
787 ;;; nnweb.el ends here