git.fiddlerwoaroof.com
Browse code

Refactor base classes, fix timezone handling

- Fix timezone handling in ALIMENTA.RSS::GET-DATE
- Refactor the base FEED and ITEM classes to use a DEFINE-COLLECTION
macro that expresses their relation better.
- Add implementations of various iterator protocols to the new
COLLECTION class used by DEFINE-COLLECTION, letting us do fancy things like
(find "the title" *some-feed* :key #'alimenta:title :test #'string-equal)
- Use the correct root element for RSS feeds

fiddlerwoaroof authored on 15/08/2016 08:28:17
Showing 9 changed files
... ...
@@ -4,18 +4,24 @@
4 4
   :description "A little library to discover, fetch, parse and generate RSS feeds"
5 5
   :author "Fiddlerwoaroof <fiddlerwoaroof@howit.is>"
6 6
   :license "MIT"
7
-  :depends-on (#:plump
8
-               #:lquery
9
-               #:should-test
10
-               #:alexandria
11
-               #:serapeum
7
+  :depends-on (#:alexandria
12 8
                #:anaphora
13 9
                #:chronicity
10
+               #:drakma
11
+               #:for
14 12
                #:fwoar.lisputils
15
-               #:split-sequence
16
-               #:drakma)
13
+               #:lquery
14
+               #:plump
15
+               #:serapeum
16
+               #:should-test
17
+               #:spinneret
18
+               #:split-sequence)
17 19
   :serial t
18 20
   :components ((:file "package")
21
+               (:file "collections")
22
+               (:file "collections-for")
23
+               #+sbcl (:file "collections-sbcl-iterators")
24
+
19 25
                (:file "alimenta")  
20 26
                (:file "data-class")
21 27
                (:file "date-handling")
... ...
@@ -3,31 +3,64 @@
3 3
 
4 4
 (in-package #:alimenta)
5 5
 
6
+(defclass feed-entity ()
7
+  ((title :initarg :title :initform nil :accessor title)  
8
+   (link :initarg :link :initform nil :accessor link)
9
+   (doc :initarg :doc :initform nil :accessor doc)))
10
+
11
+(defgeneric belongs-to (feed-entity)
12
+  (:documentation "Returns the person responsible for this feed item"))
13
+
6 14
 (defgeneric -to-feed (doc type &key feed-link)
7 15
   (:documentation "Given an xml-document, return a feed object"))
8 16
 
17
+(defgeneric render (object renderer)
18
+  (:documentation "Given a lisp object representing a feed, return it rendered
19
+                   to the specified format"))
20
+
21
+(defgeneric render-feed (feed renderer)
22
+  (:documentation "Render the container for the feed's items. Return an object
23
+                   to which the items can be added via add-rendered-item"))
24
+
25
+(defgeneric render-item (item renderer)
26
+  (:documentation "Render an item to be added to a feed. Return an object that
27
+                   can be added to the container by add-rendered-item"))
28
+
29
+(defgeneric add-rendered-item (item-representation feed-representation renderer)
30
+  (:documentation "Add the rendered item to the rendered feed"))
31
+
9 32
 (defgeneric generate-xml (feed feed-type &key partial)
10 33
   (:documentation "Given a lisp object representing a feed, return an xml
11 34
                    document"))
12 35
 
13
-(defclass feed () 
14
-  ((description :initarg :description :initform nil :accessor description)
15
-   (doc :initarg :doc :initform nil :accessor doc)
16
-   (feed-link :initarg :feed-link :initform nil :accessor feed-link)
17
-   (items :initarg :items :initform nil :accessor items)
18
-   (link :initarg :link :initform nil :accessor link)
19
-   (source-type :initarg :source-type :initform nil :accessor source-type)
20
-   (title :initarg :title :initform nil :accessor title)))
21
-
22
-(defclass item ()
36
+(defclass item (feed-entity)
23 37
   ((author :initarg :author :initform nil :accessor author)
24 38
    (content :initarg :content :initform nil :accessor content)
25 39
    (date :initarg :date :initform nil :accessor date)
26 40
    (doc :initarg :doc :initform nil :accessor doc)
27 41
    (id :initarg :id :initform nil :accessor id)
28
-   (link :initarg :link :initform nil :accessor link)
29
-   (links :initform (make-hash-table :test #'equalp) :accessor links)
30
-   (title :initarg :title :initform nil :accessor title)))
42
+   (links :initform (make-hash-table :test #'equalp) :accessor links)))
43
+
44
+(collection-class:define-collection (feed item) (feed-entity) 
45
+  ((description :initarg :description :initform nil :accessor description)
46
+   (feed-link :initarg :feed-link :initform nil :accessor feed-link)
47
+   (source-type :initarg :source-type :initform nil :accessor source-type)))
48
+
49
+(defmethod render ((feed feed) renderer)
50
+  (let ((doc (render-feed feed renderer)))
51
+    (for:for ((item over feed))
52
+      (add-rendered-item (render-item item renderer) doc renderer))))
53
+
54
+(defmethod (setf feed-link) ((value string) (feed feed))
55
+  (setf (slot-value feed 'feed-link)
56
+        (puri:parse-uri value)))
57
+
58
+(defmethod initialize-instance :after ((feed feed) &key feed-link)
59
+  (when feed-link
60
+    (setf (feed-link feed) (puri:parse-uri feed-link))))
61
+
62
+(defmethod belongs-to ((item item))
63
+  (author item))
31 64
 
32 65
 (defclass complex-value () ())
33 66
 
... ...
@@ -56,11 +89,10 @@
56 89
   (:report (lambda (condition stream)
57 90
              (format stream "Item already has link ~s" (duplicate-link-type-old condition)))))
58 91
 
59
-
60 92
 (defmethod generate-xml :around ((feed feed) feed-type &rest r)
61 93
   (declare (ignore r))
62 94
   (let ((result (call-next-method feed feed-type)))
63
-    (with-slots (items) feed
95
+    (with-accessors ((items items)) feed
64 96
       (loop for item in items
65 97
             do (generate-xml item feed-type :partial result)))
66 98
     result))
... ...
@@ -72,10 +104,10 @@
72 104
     (with-slots (doc source-type) it
73 105
       (setf doc xml-dom
74 106
             source-type doc-type))
75
-    (with-slots (items) it
76
-      (setf
77
-        items (loop for item across (get-items xml-dom doc-type)
78
-                    collect (make-item item doc-type))))))
107
+    (setf
108
+      (items it)
109
+      (loop for item across (get-items xml-dom doc-type)
110
+            collect (make-item item doc-type)))))
79 111
 
80 112
 (defgeneric (setf link) (value self))
81 113
 (defmethod (setf link) ((value cons) (self item))
... ...
@@ -144,7 +176,7 @@
144 176
 
145 177
 
146 178
 ;(defun -get-items (feed xml-dom &key type)
147
-;  (with-slots (items) feed
179
+;  (with-accessors ((items items)) feed
148 180
 ;    (loop for item across (get-items xml-dom type)
149 181
 ;          do (push (make-item xml-dom type) items)
150 182
 ;          finally (return items)))) 
... ...
@@ -173,17 +205,17 @@
173 205
     (subseq link 0 (min 30 (length link)))))
174 206
 
175 207
 (defmethod push-item ((feed feed) (item item))
176
-  (push item (slot-value feed 'items)))
208
+  (push item
209
+        (items feed)))
177 210
 
178 211
 (deftest push-item ()
179 212
   (let ((feed (make-instance 'feed))
180 213
         (item (make-instance 'item)))
181
-    (with-slots (items) feed
214
+    (with-accessors ((items items)) feed
182 215
       ;(should signal error (push-item feed 2))
183 216
       (should be eql item
184 217
               (progn
185 218
                 (push-item feed item)
186 219
                 (car items))))))
187 220
 
188
-
189 221
 ;; vim: set foldmethod=marker:
... ...
@@ -2,30 +2,30 @@
2 2
 (in-package :alimenta.atom)
3 3
 
4 4
 (defclass atom-category ()
5
-  ((term :initarg :term :initform nil)
6
-   (label :initarg :label :initform nil)
7
-   (scheme :initarg :scheme :initform nil)))
5
+  ((term :initarg :term :initform nil :accessor term)
6
+   (label :initarg :label :initform nil :accessor label)
7
+   (scheme :initarg :scheme :initform nil :accessor scheme)))
8 8
 
9 9
 (defclass atom-person ()
10
-  ((name  :initarg :name  :type (or null string) :initform nil)
11
-   (uri   :initarg :uri   :type (or null string) :initform nil)
12
-   (email :initarg :email :type (or null string) :initform nil)))
10
+  ((name  :initarg :name  :type (or null string) :initform nil :accessor name)
11
+   (uri   :initarg :uri   :type (or null string) :initform nil :accessor uri )
12
+   (email :initarg :email :type (or null string) :initform nil :accessor email)))
13 13
 
14 14
 (defclass atom-feed (alimenta:feed)
15
-  ((subtitle   :initarg :subtitle                        :initform nil)
16
-   (id         :initarg :id                              :initform nil)
17
-   (icon       :initarg :icon                            :initform nil)
18
-   (categories :initarg :categories :type (or null list) :initform nil)
19
-   (logo       :initarg :logo                            :initform nil)
20
-   (updated    :initarg :updated                         :initform nil)
21
-   (authors    :initarg :authors    :type (or null list) :initform nil)))
15
+  ((subtitle   :initarg :subtitle                        :initform nil :accessor subtitle)
16
+   (id         :initarg :id                              :initform nil :accessor id)
17
+   (icon       :initarg :icon                            :initform nil :accessor icon)
18
+   (categories :initarg :categories :type (or null list) :initform nil :accessor categories)
19
+   (logo       :initarg :logo                            :initform nil :accessor logo)
20
+   (updated    :initarg :updated                         :initform nil :accessor updated)
21
+   (authors    :initarg :authors    :type (or null list) :initform nil :accessor authors)))
22 22
 
23 23
 (defclass alimenta::link ()
24 24
   ((alimenta::relation :initarg :rel)
25 25
    (alimenta::target   :initarg :target)))
26 26
 
27 27
 (defclass atom-item (alimenta:item)
28
-  ((author-uri :initarg :author-uri :initform nil)))
28
+  ((author-uri :initarg :author-uri :initform nil :accessor author-uri)))
29 29
 
30 30
 (defun make-category (term &optional label scheme)
31 31
   (make-instance 'atom-category :term term :label label :scheme scheme))
... ...
@@ -136,19 +136,20 @@
136 136
   (let ((feed-root (or ($1 (inline partial) "feed")
137 137
                     (plump:make-element (plump:make-root) "feed"))))
138 138
     (prog1 feed-root
139
-      (with-slots (title id updated link feed-link description) feed
139
+      (with-accessors ((title title) (id id) (updated updated) (link link)
140
+                       (feed-link feed-link) (description description)) feed
140 141
         ($ (inline (make-element feed-root "title")) (text title)
141 142
 
142
-             (inline (make-element feed-root "link"))
143
-             (attr "href" feed-link) (attr "rel" "self")
143
+           (inline (make-element feed-root "link"))
144
+           (attr "href" feed-link) (attr "rel" "self")
144 145
 
145
-             (inline (make-element feed-root "link"))
146
-             (attr "href" link) (attr "rel" "alternate") (attr "type" "text/html")
146
+           (inline (make-element feed-root "link"))
147
+           (attr "href" link) (attr "rel" "alternate") (attr "type" "text/html")
147 148
 
148
-             (inline (make-element feed-root "id")) (text id) (node)
149
-             (inline (make-element feed-root "summary")) (text description) (node)
150
-             (inline (make-element feed-root "updated")) (text updated) (node)
151
-             )))))
149
+           (inline (make-element feed-root "id")) (text id) (node)
150
+           (inline (make-element feed-root "summary")) (text description) (node)
151
+           (inline (make-element feed-root "updated")) (text updated) (node)
152
+           )))))
152 153
 
153 154
 
154 155
 (defmethod generate-xml ((item item) (feed-type (eql :atom)) &key partial)
... ...
@@ -157,7 +158,8 @@
157 158
                   (plump:make-element (plump:make-root) "feed"))))
158 159
     (prog1 parent
159 160
       (let ((item-root (make-element parent "entry")))
160
-        (with-slots (title id date link content (author alimenta::author) author-uri) item
161
+        (with-accessors ((title title) (id id) (date date) (link link)
162
+                         (content content) (author author) (author-uri author-uri)) item
161 163
           ($ (inline (make-element item-root "title")) (text title)
162 164
              (inline (make-element item-root "link")) (attr "href" link)
163 165
              (inline (make-element item-root "id")) (text id) (node)
164 166
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+(defmethod for:has-more ((iterator collection-iterator))
2
+  (not (null (for:object iterator))))
3
+
4
+(defmethod for:next ((iterator collection-iterator))
5
+  (let ((collection-items (for:object iterator)))
6
+    (prog1 (car collection-items)
7
+      (setf (for:object iterator)
8
+            (cdr collection-items)))))
9
+
10
+(defmethod for:make-iterator ((collection collection) &key)
11
+  (make-instance 'collection-iterator :object collection))
12
+
13
+
0 14
new file mode 100644
... ...
@@ -0,0 +1,28 @@
1
+(in-package :collection-class)
2
+
3
+(defmethod sb-sequence:length ((sequence collection))
4
+  (length (items sequence)))
5
+
6
+(defmethod sb-sequence:elt ((sequence collection) index)
7
+  (elt (items sequence) index))
8
+
9
+(defmethod (setf sb-sequence:elt) (new-value (sequence collection) index)
10
+  (setf (elt (items sequence) index) new-value))
11
+
12
+(defmethod sb-sequence:adjust-sequence ((sequence collection) length &key initial-element initial-contents)
13
+  (let ((result (duplicate-collection sequence)))
14
+    (when (or initial-element initial-contents)
15
+      (setf (items result)
16
+            (sb-sequence:adjust-sequence (items result) length
17
+                                         :initial-element initial-element
18
+                                         :initial-contents initial-contents)))
19
+    result))
20
+
21
+(defmethod sb-sequence:make-sequence-like ((sequence collection) length &key initial-element initial-contents)
22
+  (let ((result (duplicate-collection sequence)))
23
+    (setf (items result)
24
+          (sb-sequence:make-sequence-like (items result) length
25
+                                          :initial-element initial-element
26
+                                          :initial-contents initial-contents))
27
+    result))
28
+
0 29
new file mode 100644
... ...
@@ -0,0 +1,68 @@
1
+(in-package :collection-class)
2
+
3
+(defclass collection (standard-object sequence)
4
+  ())
5
+
6
+(define-condition value-error ()
7
+  ((value :initarg :value :accessor value)))
8
+
9
+(defgeneric push-item (collection item)
10
+  (:documentation "Push item onto the beginning of the collection"))
11
+
12
+(defgeneric items (collection)
13
+  (:documentation "Get the items from a collection"))
14
+
15
+(defgeneric duplicate-collection (collection))
16
+
17
+(defgeneric copy-instance (object &rest initargs &key &allow-other-keys)
18
+  (:documentation "Makes and returns a shallow copy of OBJECT.
19
+
20
+                   An uninitialized object of the same class as OBJECT is allocated by
21
+                   calling ALLOCATE-INSTANCE.  For all slots returned by
22
+                   CLASS-SLOTS, the returned object has the
23
+                   same slot values and slot-unbound status as OBJECT.
24
+
25
+                   REINITIALIZE-INSTANCE is called to update the copy with INITARGS.")
26
+  (:method ((object standard-object) &rest initargs &key &allow-other-keys)
27
+   (let* ((class (class-of object))
28
+          (copy (allocate-instance class)))
29
+     (dolist (slot-name (mapcar #'sb-mop:slot-definition-name (sb-mop:class-slots class)))
30
+       (when (slot-boundp object slot-name)
31
+         (setf (slot-value copy slot-name)
32
+               (slot-value object slot-name))))
33
+     (apply #'reinitialize-instance copy initargs))))
34
+
35
+
36
+; TODO: actually use item-class...
37
+; TODO: finish initform handling.  Have to figure out how to make initform work with push-item
38
+(defmacro define-collection ((name item-class &key (initform '(list))) (&rest supers) &body ((&rest slots) &rest other-stuff))
39
+  (with-gensyms (item-slot-sym)
40
+    `(progn (defclass ,name (,@supers collection)
41
+              ((,item-slot-sym :initform ,initform :accessor items)
42
+               ,@slots)
43
+              ,@other-stuff)
44
+            (defmethod duplicate-collection ((collection ,name))
45
+              (let ((result (copy-instance collection)))
46
+                (setf (items result)
47
+                      (copy-seq (items result)))
48
+                result))
49
+            (defmethod push-item ((collection ,name) (item ,item-class))
50
+              (push item (items collection))))))
51
+
52
+(defclass collection-iterator (for:iterator)
53
+  ())
54
+
55
+(defmethod initialize-instance :after ((iterator collection-iterator) &key object)
56
+  (setf (for:object iterator)
57
+        (items object)))
58
+
59
+(defmethod random-item ((collection collection) &optional (random-state *random-state*))
60
+  (let* ((length (length (items collection)))
61
+         (selected-index (random length random-state)))
62
+    (elt (items collection)
63
+         selected-index)))
64
+
65
+(defmethod nth-item ((collection collection) (index integer))
66
+  (if (>= index 0)
67
+    (elt (items collection) index)
68
+    (error 'value-error :value index)))
... ...
@@ -9,9 +9,22 @@
9 9
                     drakma:*text-content-types*)))
10 10
      ,@body))
11 11
 
12
+(defvar *user-agent* "alimenta/0.0")
13
+
14
+(defun call-with-user-agent (user-agent cb &rest args)
15
+  (let ((*user-agent* user-agent))
16
+    (apply cb args)))
17
+
18
+(defun let-bind-special-var-macro-body (var value body)
19
+  `(let ((,var ,value))
20
+     ,@body))
21
+
22
+(defmacro with-user-agent ((user-agent) &body body)
23
+  (let-bind-special-var-macro-body '*user-agent* user-agent body))
24
+
12 25
 (defun fetch-doc-from-url (url)
13 26
   (setup-libraries-for-feeds 
14
-    (plump:parse (drakma:http-request url))))
27
+    (plump:parse (drakma:http-request url :user-agent *user-agent*))))
15 28
 
16 29
 (define-condition fetch-error () ())
17 30
 (define-condition feed-ambiguous (fetch-error) ((choices :initarg :choices :initform nil)))
... ...
@@ -26,7 +39,7 @@
26 39
 
27 40
 (defun fetch-feed-from-url (url &key type)
28 41
   (setup-libraries-for-feeds
29
-    (let* ((feeds (alimenta.discover:discover-feed (drakma:http-request url)))
42
+    (let* ((feeds (alimenta.discover:discover-feed (drakma:http-request url :user-agent *user-agent*)))
30 43
            (feeds (if type (remove-if-not (lambda (x) (eql type (car x))) feeds) feeds)))
31 44
       (if (not feeds) (no-feed url)
32 45
         (fetch-doc-from-url
... ...
@@ -1,5 +1,9 @@
1 1
 ;;;; package.lisp
2 2
 
3
+(defpackage #:collection-class
4
+  (:use #:cl #:alexandria #:serapeum)
5
+  (:export collection value-error push-item define-collection random-item nth-item items))
6
+
3 7
 (defpackage #:data-class
4 8
   (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora)
5 9
   (:export #:define-data-class))
... ...
@@ -8,11 +12,16 @@
8 12
   (:use #:cl #:alexandria #:serapeum #:fw.lu #:should-test))
9 13
 
10 14
 (defpackage #:alimenta
11
-  (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora)
15
+  (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:collection-class)
12 16
   (:export #:to-feed #:generate-xml #:feed #:title #:link #:items #:feed-link
13 17
            #:doc #:source-type #:id #:date #:content #:item #:description
14 18
            #:%generate-xml #:%to-feed #:get-items #:make-item #:complex-value
15
-           #:primary-value))
19
+           #:primary-value #:render #:author))
20
+
21
+(defpackage #:alimenta.html
22
+  (:use #:cl #:should-test #:lquery #:alexandria #:anaphora #:alimenta #:data-class
23
+        #:fwoar.lisputils)
24
+  (:export #:html-renderer))
16 25
 
17 26
 (defpackage #:alimenta.rss
18 27
   (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta #:data-class
... ...
@@ -48,30 +48,72 @@
48 48
             (slot-value self 'category)
49 49
             (slot-value self 'domain))))
50 50
 
51
+(defmacro check ((test &body body))
52
+  `(let ((val (progn ,@body)))
53
+     (when (,test val)
54
+       val)))
55
+
56
+(defun all-alpha (str)
57
+  (check-type str string)
58
+  (loop for char across str
59
+        always (alpha-char-p char)))
60
+
61
+
62
+(defun extract-date-timezone (date-str)
63
+  (declare (optimize (debug 3)))
64
+  (let ((tz-inited nil))
65
+    (flet ((init-tz ()
66
+             (unless tz-inited
67
+               (local-time:reread-timezone-repository)
68
+               (setf tz-inited t))))
69
+
70
+      (macrolet ((ensure-tz-inited (&body body)
71
+                   `(progn (init-tz)
72
+                           ,@body)))
73
+        (let* ((last-space (position #\space date-str :from-end t))
74
+               (tz-name (check (all-alpha (subseq date-str (1+ last-space)))))
75
+               (timestamp-raw (if tz-name
76
+                                (subseq date-str 0 last-space)
77
+                                date-str)))
78
+          (values (if tz-name
79
+                    (ensure-tz-inited
80
+                      (local-time:find-timezone-by-location-name
81
+                        (string-upcase tz-name)))
82
+                    local-time:+utc-zone+)
83
+                  timestamp-raw))))))
84
+
51 85
 (defun get-date (str)
52 86
   (declare (optimize (debug 3)))
53 87
   (handler-case
54 88
     (local-time:parse-timestring str)
55 89
     (local-time::invalid-timestring (c) (declare (ignore c))
56
-      (multiple-value-bind (res groups) (cl-ppcre:scan-to-strings "(.*)\s*([+-][0-9]{2,4})\s?$" str)
57
-        (let ((local-time:*default-timezone* local-time:+utc-zone+))
58
-          (let* ((timestamp (string-trim " " (if res (elt groups 0) str)))
59
-                 (hour-offset (if res (parse-integer (elt groups 1) :end 3) 0)) 
60
-                 (minute-offset (if (and res (> (length (elt groups 1)) 3))
61
-                                  (* (signum hour-offset) (parse-integer (elt groups 1) :start 3))
62
-                                  0)))
63
-            (loop
64
-              (restart-case (return
65
-                              (let-each (:be *)
66
-                                (chronicity:parse timestamp)
67
-                                (local-time:timestamp- * minute-offset :minute)
68
-                                (local-time:timestamp- * hour-offset   :hour)))       
69
-                (pop-token () (setf timestamp
70
-                                    (subseq timestamp
71
-                                            0
72
-                                            (position #\space timestamp
73
-                                                      :from-end t))))))))))))
74
-
90
+      (multiple-value-bind (local-time:*default-timezone* timestamp-raw) (extract-date-timezone str)
91
+        (multiple-value-bind (res groups) (cl-ppcre:scan-to-strings "(.*)\s*([+-][0-9]{2,4})\s?$" timestamp-raw)
92
+          (let ((ts (if res (elt groups 0) timestamp-raw))
93
+                (tz-offset (if res (elt groups 1) "0000")))
94
+            (let* ((timestamp (string-trim " " ts))
95
+                   ; Handle numeric timzones like -0430 or +0320
96
+                   (hour-offset (parse-integer tz-offset :end 3))
97
+                   (minute-offset (if (> (length tz-offset) 3)
98
+                                    (* (signum hour-offset)
99
+                                       (parse-integer tz-offset :start 3))
100
+                                    0)))
101
+
102
+              (loop
103
+                (restart-case (return
104
+                                (let-each (:be *)
105
+                                  (chronicity:parse timestamp)
106
+                                  (local-time:timestamp- * minute-offset :minute)
107
+                                  (local-time:timestamp- * hour-offset   :hour)))       
108
+                  (pop-token () (setf timestamp
109
+                                      (subseq timestamp
110
+                                              0
111
+                                              (position #\space timestamp
112
+                                                        :from-end t)))))))))))))
113
+
114
+(defun pop-token ()
115
+  (when-let ((restart (find-restart 'pop-token)))
116
+    (invoke-restart restart)))
75 117
 
76 118
 (defmethod primary-value ((self rss-image))
77 119
   (slot-value self 'url))
... ...
@@ -180,7 +222,7 @@
180 222
            (doc-link (get-channel-element "channel > link"))
181 223
            (doc-description (get-channel-element "channel > description"))
182 224
            (doc-feed-link (or feed-link
183
-                              ($ "feed > atom::link[rel=self]" (attr "href") (node)))))
225
+                              ($ "channel > atom::link[rel=self]" (attr "href") (node)))))
184 226
       (make-instance 'rss-feed
185 227
         :title doc-title
186 228
         :link doc-link