1 ;;; nnweb.el --- retrieving articles via web search engines
2 ;; Copyright (C) 1996, 1997, 1998, 1999, 2000
3 ;; Free Software Foundation, Inc.
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
8 ;; This file is part of GNU Emacs.
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING. If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
27 ;; Note: You need to have `url' and `w3' installed for this
32 (eval-when-compile (require 'cl
))
46 ;; Report failure to find w3 at load time if appropriate.
47 (unless noninteractive
51 (require 'w3-forms
))))
55 (defvoo nnweb-directory
(nnheader-concat gnus-directory
"nnweb/")
56 "Where nnweb will save its files.")
58 (defvoo nnweb-type
'dejanews
59 "What search engine type is being used.
60 Valid types include `dejanews', `dejanewsold', `reference',
63 (defvar nnweb-type-definition
66 (id .
"http://search.dejanews.com/msgid.xp?MID=%s&fmt=text")
67 (map . nnweb-dejanews-create-mapping
)
68 (search . nnweb-dejanews-search
)
69 (address .
"http://www.deja.com/=dnc/qs.xp")
70 (identifier . nnweb-dejanews-identity
))
73 (map . nnweb-dejanews-create-mapping
)
74 (search . nnweb-dejanewsold-search
)
75 (address .
"http://www.deja.com/dnquery.xp")
76 (identifier . nnweb-dejanews-identity
))
78 (article . nnweb-reference-wash-article
)
79 (map . nnweb-reference-create-mapping
)
80 (search . nnweb-reference-search
)
81 (address .
"http://www.reference.com/cgi-bin/pn/go")
82 (identifier . identity
))
84 (article . nnweb-altavista-wash-article
)
85 (map . nnweb-altavista-create-mapping
)
86 (search . nnweb-altavista-search
)
87 (address .
"http://www.altavista.digital.com/cgi-bin/query")
88 (id .
"/cgi-bin/news?id@%s")
89 (identifier . identity
)))
90 "Type-definition alist.")
92 (defvoo nnweb-search nil
93 "Search string to feed to DejaNews.")
95 (defvoo nnweb-max-hits
999
96 "Maximum number of hits to display.")
98 (defvoo nnweb-ephemeral-p nil
99 "Whether this nnweb server is ephemeral.")
101 ;;; Internal variables
103 (defvoo nnweb-articles nil
)
104 (defvoo nnweb-buffer nil
)
105 (defvoo nnweb-group-alist nil
)
106 (defvoo nnweb-group nil
)
107 (defvoo nnweb-hashtb nil
)
109 ;;; Interface functions
111 (nnoo-define-basics nnweb
)
113 (deffoo nnweb-retrieve-headers
(articles &optional group server fetch-old
)
114 (nnweb-possibly-change-server group server
)
116 (set-buffer nntp-server-buffer
)
118 (let (article header
)
119 (mm-with-unibyte-current-buffer
120 (while (setq article
(pop articles
))
121 (when (setq header
(cadr (assq article nnweb-articles
)))
122 (nnheader-insert-nov header
))))
125 (deffoo nnweb-request-scan
(&optional group server
)
126 (nnweb-possibly-change-server group server
)
127 (funcall (nnweb-definition 'map
))
128 (unless nnweb-ephemeral-p
130 (nnweb-write-overview group
)))
132 (deffoo nnweb-request-group
(group &optional server dont-check
)
133 (nnweb-possibly-change-server nil server
)
135 (not (equal group nnweb-group
))
136 (not nnweb-ephemeral-p
))
137 (let ((info (assoc group nnweb-group-alist
)))
139 (setq nnweb-group group
)
140 (setq nnweb-type
(nth 2 info
))
141 (setq nnweb-search
(nth 3 info
))
143 (nnweb-read-overview group
)))))
145 ((not nnweb-articles
)
146 (nnheader-report 'nnweb
"No matching articles"))
148 (let ((active (if nnweb-ephemeral-p
149 (cons (caar nnweb-articles
)
150 (caar (last nnweb-articles
)))
151 (cadr (assoc group nnweb-group-alist
)))))
152 (nnheader-report 'nnweb
"Opened group %s" group
)
154 "211 %d %d %d %s\n" (length nnweb-articles
)
155 (car active
) (cdr active
) group
)))))
157 (deffoo nnweb-close-group
(group &optional server
)
158 (nnweb-possibly-change-server group server
)
159 (when (gnus-buffer-live-p nnweb-buffer
)
161 (set-buffer nnweb-buffer
)
162 (set-buffer-modified-p nil
)
163 (kill-buffer nnweb-buffer
)))
166 (deffoo nnweb-request-article
(article &optional group server buffer
)
167 (nnweb-possibly-change-server group server
)
169 (set-buffer (or buffer nntp-server-buffer
))
170 (let* ((header (cadr (assq article nnweb-articles
)))
171 (url (and header
(mail-header-xref header
))))
173 (mm-with-unibyte-current-buffer
174 (nnweb-fetch-url url
)))
175 (and (stringp article
)
176 (nnweb-definition 'id t
)
177 (let ((fetch (nnweb-definition 'id
))
179 (when (string-match "^<\\(.*\\)>$" article
)
180 (setq art
(match-string 1 article
)))
183 (mm-with-unibyte-current-buffer
185 (format fetch article
)))))))
186 (unless nnheader-callback-function
187 (funcall (nnweb-definition 'article
))
188 (nnweb-decode-entities))
189 (nnheader-report 'nnweb
"Fetched article %s" article
)
190 (cons group
(and (numberp article
) article
))))))
192 (deffoo nnweb-close-server
(&optional server
)
193 (when (and (nnweb-server-opened server
)
194 (gnus-buffer-live-p nnweb-buffer
))
196 (set-buffer nnweb-buffer
)
197 (set-buffer-modified-p nil
)
198 (kill-buffer nnweb-buffer
)))
199 (nnoo-close-server 'nnweb server
))
201 (deffoo nnweb-request-list
(&optional server
)
202 (nnweb-possibly-change-server nil server
)
204 (set-buffer nntp-server-buffer
)
205 (nnmail-generate-active nnweb-group-alist
)
208 (deffoo nnweb-request-update-info
(group info
&optional server
)
209 (nnweb-possibly-change-server group server
))
211 (deffoo nnweb-asynchronous-p
()
214 (deffoo nnweb-request-create-group
(group &optional server args
)
215 (nnweb-possibly-change-server nil server
)
216 (nnweb-request-delete-group group
)
217 (push `(,group
,(cons 1 0) ,@args
) nnweb-group-alist
)
221 (deffoo nnweb-request-delete-group
(group &optional force server
)
222 (nnweb-possibly-change-server group server
)
223 (gnus-pull group nnweb-group-alist t
)
225 (gnus-delete-file (nnweb-overview-file group
))
228 (nnoo-define-skeleton nnweb
)
230 ;;; Internal functions
232 (defun nnweb-read-overview (group)
233 "Read the overview of GROUP and build the map."
234 (when (file-exists-p (nnweb-overview-file group
))
235 (mm-with-unibyte-buffer
236 (nnheader-insert-file-contents (nnweb-overview-file group
))
237 (goto-char (point-min))
240 (setq header
(nnheader-parse-nov))
242 (push (list (mail-header-number header
)
243 header
(mail-header-xref header
))
245 (nnweb-set-hashtb header
(car nnweb-articles
)))))))
247 (defun nnweb-write-overview (group)
248 "Write the overview file for GROUP."
249 (with-temp-file (nnweb-overview-file group
)
250 (let ((articles nnweb-articles
))
252 (nnheader-insert-nov (cadr (pop articles
)))))))
254 (defun nnweb-set-hashtb (header data
)
255 (gnus-sethash (nnweb-identifier (mail-header-xref header
))
258 (defun nnweb-get-hashtb (url)
259 (gnus-gethash (nnweb-identifier url
) nnweb-hashtb
))
261 (defun nnweb-identifier (ident)
262 (funcall (nnweb-definition 'identifier
) ident
))
264 (defun nnweb-overview-file (group)
265 "Return the name of the overview file of GROUP."
266 (nnheader-concat nnweb-directory group
".overview"))
268 (defun nnweb-write-active ()
269 "Save the active file."
270 (gnus-make-directory nnweb-directory
)
271 (with-temp-file (nnheader-concat nnweb-directory
"active")
272 (prin1 `(setq nnweb-group-alist
',nnweb-group-alist
) (current-buffer))))
274 (defun nnweb-read-active ()
275 "Read the active file."
276 (load (nnheader-concat nnweb-directory
"active") t t t
))
278 (defun nnweb-definition (type &optional noerror
)
279 "Return the definition of TYPE."
280 (let ((def (cdr (assq type
(assq nnweb-type nnweb-type-definition
)))))
283 (error "Undefined definition %s" type
))
286 (defun nnweb-possibly-change-server (&optional group server
)
289 (unless (nnweb-server-opened server
)
290 (nnweb-open-server server
)))
291 (unless nnweb-group-alist
294 (when (and (not nnweb-ephemeral-p
)
295 (not (equal group nnweb-group
)))
296 (setq nnweb-hashtb
(gnus-make-hashtable 4095))
297 (nnweb-request-group group nil t
))))
299 (defun nnweb-init (server)
300 "Initialize buffers and such."
301 (unless (gnus-buffer-live-p nnweb-buffer
)
305 (nnheader-set-temp-buffer
306 (format " *nnweb %s %s %s*"
307 nnweb-type nnweb-search server
))
308 (current-buffer))))))
310 (defun nnweb-fetch-url (url)
313 (if (not nnheader-callback-function
)
316 (mm-enable-multibyte)
317 (let ((coding-system-for-read 'binary
)
318 (coding-system-for-write 'binary
)
319 (default-process-coding-system 'binary
))
321 (setq buf
(buffer-string)))
325 (nnweb-url-retrieve-asynch
326 url
'nnweb-callback
(current-buffer) nnheader-callback-function
)
329 (defun nnweb-callback (buffer callback
)
330 (when (gnus-buffer-live-p url-working-buffer
)
332 (set-buffer url-working-buffer
)
333 (funcall (nnweb-definition 'article
))
334 (nnweb-decode-entities)
336 (goto-char (point-max))
337 (insert-buffer-substring url-working-buffer
))
339 (gnus-kill-buffer url-working-buffer
)))
341 (defun nnweb-url-retrieve-asynch (url callback
&rest data
)
342 (let ((url-request-method "GET")
343 (old-asynch url-be-asynchronous
)
344 (url-request-data nil
)
345 (url-request-extra-headers nil
)
346 (url-working-buffer (generate-new-buffer-name " *nnweb*")))
347 (setq-default url-be-asynchronous t
)
349 (set-buffer (get-buffer-create url-working-buffer
))
350 (setq url-current-callback-data data
351 url-be-asynchronous t
352 url-current-callback-func callback
)
354 (setq-default url-be-asynchronous old-asynch
)))
357 ;;; DejaNews functions.
360 (defun nnweb-dejanews-create-mapping ()
361 "Perform the search and create an number-to-url alist."
363 (set-buffer nnweb-buffer
)
365 (when (funcall (nnweb-definition 'search
) nnweb-search
)
369 (active (or (cadr (assoc nnweb-group nnweb-group-alist
))
372 map url parse a table group text
)
374 ;; Go through all the article hits on this page.
375 (goto-char (point-min))
376 (setq parse
(w3-parse-buffer (current-buffer))
377 table
(nth 1 (nnweb-parse-find-all 'table parse
)))
378 (dolist (row (nth 2 (car (nth 2 table
))))
379 (setq a
(nnweb-parse-find 'a row
)
380 url
(cdr (assq 'href
(nth 1 a
)))
381 text
(nreverse (nnweb-text row
)))
383 (setq subject
(nth 4 text
)
387 (if (string-match "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]+\\)" date
)
388 (setq date
(format "%s %s 00:00:00 %s"
389 (car (rassq (string-to-number
390 (match-string 2 date
))
392 (match-string 3 date
)
393 (match-string 1 date
)))
394 (setq date
"Jan 1 00:00:00 0000"))
396 (setq url
(concat url
"&fmt=text"))
397 (when (string-match "&context=[^&]+" url
)
398 (setq url
(replace-match "" t t url
)))
399 (unless (nnweb-get-hashtb url
)
403 (make-full-mail-header
404 (cdr active
) (concat subject
" (" group
")") from date
405 (concat "<" (nnweb-identifier url
) "@dejanews>")
408 (nnweb-set-hashtb (cadar map
) (car map
)))))
409 ;; See whether there is a "Get next 20 hits" button here.
410 (goto-char (point-min))
411 (if (or (not (re-search-forward
412 "HREF=\"\\([^\"]+\\)\"[<>b]+Next result" nil t
))
413 (>= i nnweb-max-hits
))
416 (setq more
(match-string 1))
418 (url-insert-file-contents more
)))
419 ;; Return the articles in the right order.
421 (sort (nconc nnweb-articles map
) 'car-less-than-car
))))))
423 (defun nnweb-dejanews-search (search)
426 (nnweb-definition 'address
)
428 (nnweb-encode-www-form-urlencoded
430 ("svcclass" .
"dnyr")
432 ("defaultOp" .
"AND")
434 ("OP" .
"dnquery.xp")
438 ("format" .
"verbose2")
439 ("showsort" .
"date")
441 ("ageweight" .
"1")))))
444 (defun nnweb-dejanewsold-search (search)
446 (nnweb-definition 'address
)
447 `(("query" .
,search
)
448 ("defaultOp" .
"AND")
449 ("svcclass" .
"dnold")
451 ("format" .
"verbose2")
453 ("showsort" .
"date")
455 ("ageweight" .
"1")))
458 (defun nnweb-dejanews-identity (url)
459 "Return an unique identifier based on URL."
460 (if (string-match "AN=\\([0-9]+\\)" url
)
468 (defun nnweb-reference-create-mapping ()
469 "Perform the search and create an number-to-url alist."
471 (set-buffer nnweb-buffer
)
473 (when (funcall (nnweb-definition 'search
) nnweb-search
)
477 (active (or (cadr (assoc nnweb-group nnweb-group-alist
))
479 Subject Score Date Newsgroups From Message-ID
482 ;; Go through all the article hits on this page.
483 (goto-char (point-min))
484 (search-forward "</pre><hr>" nil t
)
485 (delete-region (point-min) (point))
486 (goto-char (point-min))
487 (while (re-search-forward "^ +[0-9]+\\." nil t
)
490 (if (re-search-forward "^$" nil t
)
493 (goto-char (point-min))
494 (when (looking-at ".*href=\"\\([^\"]+\\)\"")
495 (setq url
(match-string 1)))
496 (nnweb-remove-markup)
497 (goto-char (point-min))
498 (while (search-forward "\t" nil t
)
500 (goto-char (point-min))
501 (while (re-search-forward "^\\([^:]+\\): \\(.*\\)$" nil t
)
502 (set (intern (match-string 1)) (match-string 2)))
504 (search-forward "</pre>" nil t
)
506 (unless (nnweb-get-hashtb url
)
510 (make-full-mail-header
511 (cdr active
) (concat "(" Newsgroups
") " Subject
) From Date
513 nil
0 (string-to-int Score
) url
))
515 (nnweb-set-hashtb (cadar map
) (car map
))))
517 ;; Return the articles in the right order.
519 (sort (nconc nnweb-articles map
) 'car-less-than-car
))))))
521 (defun nnweb-reference-wash-article ()
522 (let ((case-fold-search t
))
523 (goto-char (point-min))
524 (re-search-forward "^</center><hr>" nil t
)
525 (delete-region (point-min) (point))
526 (search-forward "<pre>" nil t
)
528 (let ((body (point-marker)))
529 (search-forward "</pre>" nil t
)
530 (delete-region (point) (point-max))
531 (nnweb-remove-markup)
532 (goto-char (point-min))
533 (while (looking-at " *$")
535 (narrow-to-region (point-min) body
)
536 (while (and (re-search-forward "^$" nil t
)
539 (goto-char (point-min))
540 (while (looking-at "\\(^[^ ]+:\\) *")
541 (replace-match "\\1 " t
)
543 (goto-char (point-min))
544 (when (re-search-forward "^References:" nil t
)
546 (point) (if (re-search-forward "^$\\|^[^:]+:" nil t
)
549 (goto-char (point-min))
551 (unless (looking-at "References")
554 (goto-char (point-min))
555 (while (search-forward "," nil t
)
556 (replace-match " " t t
)))
558 (set-marker body nil
))))
560 (defun nnweb-reference-search (search)
561 (url-insert-file-contents
563 (nnweb-definition 'address
)
565 (nnweb-encode-www-form-urlencoded
566 `(("search" .
"advanced")
567 ("querytext" .
,search
)
572 ("organization" .
"")
575 ("choice" .
"Search")
576 ("startmonth" .
"Jul")
578 ("startyear" .
"1996")
583 ("verbosity" .
"Verbose")
584 ("ranking" .
"Relevance")
588 (setq buffer-file-name nil
)
595 (defun nnweb-altavista-create-mapping ()
596 "Perform the search and create an number-to-url alist."
598 (set-buffer nnweb-buffer
)
601 (when (funcall (nnweb-definition 'search
) nnweb-search part
)
605 (active (or (cadr (assoc nnweb-group nnweb-group-alist
))
607 subject date from id group
610 ;; Go through all the article hits on this page.
611 (goto-char (point-min))
612 (search-forward "<dt>" nil t
)
613 (delete-region (point-min) (match-beginning 0))
614 (goto-char (point-min))
615 (while (search-forward "<dt>" nil t
)
616 (replace-match "\n<blubb>"))
617 (nnweb-decode-entities)
618 (goto-char (point-min))
619 (while (re-search-forward "<blubb>.*href=\"\\([^\"]+\\)\"><strong>\\([^>]*\\)</strong></a><dd>\\([^-]+\\)- <b>\\([^<]+\\)<.*href=\"news:\\([^\"]+\\)\">.*\">\\(.+\\)</a><P>"
621 (setq url
(match-string 1)
622 subject
(match-string 2)
623 date
(match-string 3)
624 group
(match-string 4)
625 id
(concat "<" (match-string 5) ">")
626 from
(match-string 6))
628 (unless (nnweb-get-hashtb url
)
632 (make-full-mail-header
633 (cdr active
) (concat "(" group
") " subject
) from date
636 (nnweb-set-hashtb (cadar map
) (car map
))))
637 ;; See if we want more.
638 (when (or (not nnweb-articles
)
639 (>= i nnweb-max-hits
)
640 (not (funcall (nnweb-definition 'search
)
641 nnweb-search
(incf part
))))
643 ;; Return the articles in the right order.
645 (sort (nconc nnweb-articles map
) 'car-less-than-car
)))))))
647 (defun nnweb-altavista-wash-article ()
648 (goto-char (point-min))
649 (let ((case-fold-search t
))
650 (when (re-search-forward "^<strong>" nil t
)
651 (delete-region (point-min) (match-beginning 0)))
652 (goto-char (point-min))
653 (while (looking-at "<strong>\\([^ ]+\\) +</strong> +\\(.*\\)$")
654 (replace-match "\\1: \\2" t
)
656 (when (re-search-backward "^References:" nil t
)
657 (narrow-to-region (point) (progn (forward-line 1) (point)))
658 (goto-char (point-min))
659 (while (re-search-forward "<A.*\\?id@\\([^\"]+\\)\">[0-9]+</A>" nil t
)
660 (replace-match "<\\1> " t
)))
662 (nnweb-remove-markup)))
664 (defun nnweb-altavista-search (search &optional part
)
665 (url-insert-file-contents
667 (nnweb-definition 'address
)
669 (nnweb-encode-www-form-urlencoded
672 ,@(when part
`(("stq" .
,(int-to-string (* part
30)))))
678 (setq buffer-file-name nil
)
682 ;;; General web/w3 interface utility functions
685 (defun nnweb-insert-html (parse)
686 "Insert HTML based on a w3 parse tree."
689 (insert "<" (symbol-name (car parse
)) " ")
692 (concat (symbol-name (car param
)) "="
694 (if (consp (cdr param
))
700 (mapcar 'nnweb-insert-html
(nth 2 parse
))
701 (insert "</" (symbol-name (car parse
)) ">\n")))
703 (defun nnweb-encode-www-form-urlencoded (pairs)
704 "Return PAIRS encoded for forms."
708 (concat (w3-form-encode-xwfu (car data
)) "="
709 (w3-form-encode-xwfu (cdr data
)))))
712 (defun nnweb-fetch-form (url pairs
)
713 "Fetch a form from URL with PAIRS as the data using the POST method."
714 (let ((url-request-data (nnweb-encode-www-form-urlencoded pairs
))
715 (url-request-method "POST")
716 (url-request-extra-headers
717 '(("Content-type" .
"application/x-www-form-urlencoded"))))
718 (url-insert-file-contents url
)
719 (setq buffer-file-name nil
))
722 (defun nnweb-decode-entities ()
723 "Decode all HTML entities."
724 (goto-char (point-min))
725 (while (re-search-forward "&\\(#[0-9]+\\|[a-z]+\\);" nil t
)
726 (replace-match (char-to-string
727 (if (eq (aref (match-string 1) 0) ?\
#)
729 (string-to-number (substring
730 (match-string 1) 1))))
731 (if (mm-char-or-char-int-p c
) c
32))
732 (or (cdr (assq (intern (match-string 1))
737 (defun nnweb-decode-entities-string (str)
740 (nnweb-decode-entities)
741 (buffer-substring (point-min) (point-max))))
743 (defun nnweb-remove-markup ()
744 "Remove all HTML markup, leaving just plain text."
745 (goto-char (point-min))
746 (while (search-forward "<!--" nil t
)
747 (delete-region (match-beginning 0)
748 (or (search-forward "-->" nil t
)
750 (goto-char (point-min))
751 (while (re-search-forward "<[^>]+>" nil t
)
752 (replace-match "" t t
)))
754 (defun nnweb-insert (url &optional follow-refresh
)
755 "Insert the contents from an URL in the current buffer.
756 If FOLLOW-REFRESH is non-nil, redirect refresh url in META."
757 (let ((name buffer-file-name
))
760 (narrow-to-region (point) (point))
761 (url-insert-file-contents url
)
762 (goto-char (point-min))
763 (when (re-search-forward
764 "<meta[ \t\r\n]*http-equiv=\"Refresh\"[^>]*URL=\\([^\"]+\\)\"" nil t
)
765 (let ((url (match-string 1)))
766 (delete-region (point-min) (point-max))
767 (nnweb-insert url t
))))
768 (url-insert-file-contents url
))
769 (setq buffer-file-name name
)))
771 (defun nnweb-parse-find (type parse
&optional maxdepth
)
772 "Find the element of TYPE in PARSE."
774 (nnweb-parse-find-1 type parse maxdepth
)))
776 (defun nnweb-parse-find-1 (type contents maxdepth
)
777 (when (or (null maxdepth
)
778 (not (zerop maxdepth
)))
779 (when (consp contents
)
780 (when (eq (car contents
) type
)
781 (throw 'found contents
))
782 (when (listp (cdr contents
))
783 (dolist (element contents
)
784 (when (consp element
)
785 (nnweb-parse-find-1 type element
786 (and maxdepth
(1- maxdepth
)))))))))
788 (defun nnweb-parse-find-all (type parse
)
789 "Find all elements of TYPE in PARSE."
791 (nnweb-parse-find-all-1 type parse
)))
793 (defun nnweb-parse-find-all-1 (type contents
)
795 (when (consp contents
)
796 (if (eq (car contents
) type
)
797 (push contents result
)
798 (when (listp (cdr contents
))
799 (dolist (element contents
)
800 (when (consp element
)
802 (nconc result
(nnweb-parse-find-all-1 type element
))))))))
806 (defun nnweb-text (parse)
807 "Return a list of text contents in PARSE."
808 (let ((nnweb-text nil
))
810 (nreverse nnweb-text
)))
812 (defun nnweb-text-1 (contents)
813 (dolist (element contents
)
814 (if (stringp element
)
815 (push element nnweb-text
)
816 (when (and (consp element
)
817 (listp (cdr element
)))
818 (nnweb-text-1 element
)))))
822 ;;; nnweb.el ends here