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