git.fiddlerwoaroof.com
Browse code

Splitting RSS support out of main lisp file

- Added local-time based parsing and printing of dates.

- Changed specialization of %generate-xml so that it can generate an RSS
feed from an arbitrary object.

fiddlerwoaroof authored on 09/02/2016 05:13:53
Showing 7 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
1
+.*.sw[a-z]
2
+*~
... ...
@@ -14,6 +14,7 @@
14 14
   :components ((:file "package")
15 15
                (:file "alimenta")  
16 16
                (:file "atom")  
17
+               (:file "rss")  
17 18
                (:file "fetching")
18 19
                (:file "discovery")))
19 20
 
... ...
@@ -1,30 +1,107 @@
1 1
 ;;;; alimenta.lisp
2 2
 (declaim (optimize (speed 0) (safety 3) (debug 3)))
3 3
 
4
-
5 4
 (in-package #:alimenta)
6 5
 
7
-;;; "alimenta" goes here. Hacks and glory await!
8
-(defun detect-feed-type (xml-dom)
9
-  (let ((root-node-name (make-keyword (string-upcase
10
-                                        ($ (inline xml-dom) (children)
11
-                                           (map #'tag-name) (node))))))
12
-    (setf type
13
-          (case root-node-name
14
-            ((:feed) :atom)
15
-            (t root-node-name)))))
6
+(defclass feed () 
7
+  ((title :initarg :title :initform nil)
8
+   (link :initarg :link :initform nil)
9
+   (items :initarg :items :initform nil)
10
+   (description :initarg :description :initform nil)
11
+   (feed-link :initarg :feed-link :initform nil)
12
+   (doc :initarg :doc :initform nil)
13
+   (source-type :initarg :source-type :initform nil)))
14
+
15
+(defclass item ()
16
+  ((title :initarg :title :initform nil)
17
+   (id :initarg :id :initform nil)
18
+   (author :initarg :author :initform nil)
19
+   (date :initarg :date :initform nil)
20
+   (link :initarg :link :initform nil)
21
+   (links :initform (make-hash-table :test #'equalp))
22
+   (content :initarg :content :initform nil)
23
+   (doc :initarg :doc :initform nil)))
24
+
25
+(define-condition duplicate-link-type (error)
26
+  ((old :reader duplicate-link-type-old :initarg :old)
27
+   (new :reader duplicate-link-type-new :initarg :new))
28
+  (:report (lambda (condition stream)
29
+             (format stream "Item already has link ~s" (duplicate-link-type-old condition)))))
16 30
 
17 31
 
18 32
 (defgeneric push-item (feed item)
19 33
   (:documentation "Adds an item to the feed"))
20 34
 
21
-(defgeneric make-item (xml-dom type))
35
+(defgeneric make-item (xml-dom doc-type)
36
+  (:documentation "Given an xml document, return an item"))
22 37
 
23 38
 (defgeneric parse-feed (feed))
24 39
 
25
-(defgeneric %to-feed (doc type &key feed-link))
40
+(defgeneric %get-items (xml feed-type))
26 41
 
27 42
 (defgeneric %generate-xml (feed feed-type &key partial))
43
+(defmethod %generate-xml :around ((feed feed) feed-type &rest r)
44
+  (declare (ignore r))
45
+  (let ((result (call-next-method feed feed-type)))
46
+    (with-slots (items) feed
47
+      (loop for item in items
48
+            do (%generate-xml item feed-type :partial result)))
49
+    result))
50
+
51
+(defgeneric %to-feed (doc type &key feed-link)
52
+  (:documentation "Given an xml-document, return a feed object"))
53
+(defmethod %to-feed :around ((xml-dom plump:node) doc-type &key feed-link)
54
+  "This wraps the particular methods so that _they_ don't have to implement item fetching.
55
+   NIL passed to the type activates auto-detection"
56
+  (aprog1 (call-next-method xml-dom doc-type :feed-link feed-link)
57
+    (with-slots (doc source-type) it
58
+      (setf doc xml-dom
59
+            source-type doc-type))
60
+    (with-slots (items) it
61
+      (setf
62
+        items (loop for item across (%get-items xml-dom doc-type)
63
+                    collect (make-item item doc-type))))))
64
+
65
+(defgeneric (setf link) (value self))
66
+(defmethod (setf link) ((value cons) (self item))
67
+  (with-slots (links) self
68
+    (destructuring-bind (type . href) value
69
+      (when (consp href)
70
+        (if (null (cdr href))
71
+          (setf href (car href))
72
+          (error 'type-error "too many arguments")))
73
+      (let ((type-keyword (make-keyword (string-upcase type))))
74
+        (when (slot-boundp self 'links)
75
+          (multiple-value-bind (old-link old-link-p) (gethash type-keyword links) 
76
+            (when old-link-p
77
+              (cerror "Replace Link ~a:~a with ~a:~a" 'duplicate-link-type :old old-link :new href))))
78
+        (setf (gethash type-keyword links) href)))))
79
+
80
+(defmethod print-object ((object feed) stream)
81
+  (print-unreadable-object (object stream :type t :identity t)
82
+    (with-slots (title link) object
83
+      (format stream "title: ~s link: ~s"
84
+              (aif title (shorten-link it) "<untitled>")
85
+              (aif link (shorten-link it) "<no link>")))))
86
+
87
+(defmethod print-object ((object item) stream)
88
+  (print-unreadable-object (object stream :type t :identity t)
89
+    (with-slots (title link date) object
90
+      (format stream "title: ~s link: ~s date:~s"
91
+              (aif title (shorten-link it) "<untitled>")
92
+              (aif link (shorten-link it) "<no link>")
93
+              (aif date it "<no date>")))))
94
+
95
+
96
+(defun detect-feed-type (xml-dom)
97
+  (let ((root-node-name (make-keyword (string-upcase
98
+                                        ($ (inline xml-dom) (children)
99
+                                           (map #'tag-name) (node))))))
100
+    (setf type
101
+          (case root-node-name
102
+            ((:feed) :atom)
103
+            (t root-node-name)))))
104
+
28 105
 
29 106
 (defun generate-xml (feed &key (feed-type :rss))
30 107
   (%generate-xml feed feed-type))
... ...
@@ -37,8 +114,6 @@
37 114
     (setf type (detect-feed-type doc)))
38 115
   (%to-feed doc type :feed-link feed-link))
39 116
 
40
-(defgeneric %get-items (xml feed-type)
41
-  (:method (xml-dom (feed-type (eql :rss))) ($ (inline xml-dom) "channel > item")))
42 117
 
43 118
 (defun get-items (feed xml-dom &key type)
44 119
   (with-slots (items) feed
... ...
@@ -46,18 +121,6 @@
46 121
           do (push (make-item xml-dom type) items)
47 122
           finally (return items)))) 
48 123
 
49
-(defmethod %to-feed :around (xml-dom doc-type &key feed-link)
50
-  "This wraps the particular methods so that _they_ don't have to implement item fetching.
51
-   NIL passed to the type activates auto-detection"
52
-  (aprog1 (call-next-method xml-dom doc-type :feed-link feed-link)
53
-    (with-slots (doc source-type) it
54
-      (setf doc xml-dom
55
-            source-type doc-type))
56
-    (with-slots (items) it
57
-      (setf
58
-        items (loop for item across (%get-items xml-dom doc-type)
59
-                    collect (make-item item doc-type))))))
60
-
61 124
 (defgeneric feed-to-rss (feed))
62 125
 (defgeneric feed-to-atom (feed))
63 126
 (defgeneric feed-to-json (feed))
... ...
@@ -82,15 +145,6 @@
82 145
      </body>
83 146
      </html>"))
84 147
 
85
-(defclass feed ()
86
-  ((title :initarg :title :initform nil)
87
-   (link :initarg :link :initform nil)
88
-   (items :initarg :items :initform nil)
89
-   (description :initarg :description :initform nil)
90
-   (feed-link :initarg :feed-link :initform nil)
91
-   (doc :initarg :doc :initform nil)
92
-   (source-type :initarg :source-type :initform nil)))
93
-
94 148
 (defun make-feed (&key title link items feed-link description)
95 149
   (make-instance 'feed :title title :link link :items items :feed-link feed-link :description description))
96 150
 
... ...
@@ -105,135 +159,10 @@
105 159
     (push-item feed it)
106 160
     (values feed it)))
107 161
 
108
-(defmethod %generate-xml :around ((feed feed) feed-type &rest r)
109
-  (declare (ignore r))
110
-  (let ((result (call-next-method feed feed-type)))
111
-    (with-slots (items) feed
112
-      (loop for item in items
113
-            do (%generate-xml item feed-type :partial result)))
114
-    result))
115
-
116
-(defmethod shorten-link (link)
162
+(defun shorten-link (link)
117 163
   (let ((link (cl-ppcre:regex-replace "^https?:" link "")))
118 164
     (subseq link 0 (min 30 (length link)))))
119 165
 
120
-(defmethod print-object ((object feed) stream)
121
-  (print-unreadable-object (object stream :type t :identity t)
122
-    (with-slots (title link) object
123
-      (format stream "title: ~s link: ~s"
124
-              (aif title (shorten-link it) "<untitled>")
125
-              (aif link (shorten-link it) "<no link>")))))
126
-
127
-(defclass rss-feed (feed) ())
128
-
129
-(defmethod %generate-xml ((feed rss-feed) (feed-type (eql :rss)) &rest r)
130
-  (declare (ignore r))
131
-  (let* ((xml-root (plump:make-root))
132
-         (feed-root (plump:make-element xml-root "rss"))
133
-         (channel (plump-dom:make-element feed-root "channel")))
134
-    ($ (inline feed-root)
135
-       (attr "version" "2.0")
136
-       (attr "xmlns:content" "http://purl.org/rss/1.0/modules/content/")
137
-       (attr "xmlns:atom" "http://www.w3.org/2005/Atom"))
138
-    (with-slots (title link feed-link description) feed
139
-      ($ (inline (make-element channel "title"))
140
-         (text title))
141
-      ($ (inline (make-element channel "link"))
142
-         (text link))
143
-      (awhen description
144
-        ($ (inline (make-element channel "description"))
145
-         (text it)))
146
-      ($ (inline (make-element channel "atom:link"))
147
-         (attr "rel" "self")
148
-         (attr "type" "application/rss+xml")
149
-         (attr "href" link)))
150
-    xml-root))
151
-
152
-(defclass item ()
153
-  ((title :initarg :title :initform nil)
154
-   (id :initarg :id :initform nil)
155
-   (author :initarg :author :initform nil)
156
-   (date :initarg :date :initform nil)
157
-   (link :initarg :link :initform nil)
158
-   (links :initform (make-hash-table :test #'equalp))
159
-   (content :initarg :content :initform nil)
160
-   (doc :initarg :doc :initform nil)))
161
-
162
-(defgeneric (setf link) (value self))
163
-
164
-(define-condition duplicate-link-type (error)
165
-  ((old :reader duplicate-link-type-old :initarg :old)
166
-   (new :reader duplicate-link-type-new :initarg :new))
167
-  (:report (lambda (condition stream)
168
-             (format stream "Item already has link ~s" (duplicate-link-type-old condition)))))
169
-
170
-(defmethod (setf link) ((value cons) (self item))
171
-  (with-slots (links) self
172
-    (destructuring-bind (type . href) value
173
-      (when (consp href)
174
-        (if (null (cdr href))
175
-          (setf href (car href))
176
-          (error 'type-error "too many arguments")))
177
-      (let ((type-keyword (make-keyword (string-upcase type))))
178
-        (when (slot-boundp self 'links)
179
-          (multiple-value-bind (old-link old-link-p) (gethash type-keyword links) 
180
-            (when old-link-p
181
-              (cerror "Replace Link ~a:~a with ~a:~a" 'duplicate-link-type :old old-link :new href))))
182
-        (setf (gethash type-keyword links) href)))))
183
-
184
-(defmethod %generate-xml ((item item) (feed-type (eql :rss)) &key partial)
185
-  (prog1 partial
186
-    (let ((item-root (make-element ($ (inline partial) "channel" (node)) "item")))
187
-      (with-slots (title id date link content) item
188
-        ($ (inline (make-element item-root "title")) (text title)) 
189
-        ($ (inline (make-element item-root "link")) (text link)) 
190
-        (plump-dom:set-attribute
191
-          ($ (inline (make-element item-root "guid")) (text id) (node))
192
-          "isPermaLink"
193
-          "false") 
194
-        ($ (inline (make-element item-root "pubDate")) (text date)) 
195
-        ($ (inline (make-element item-root "description")) (text content))))))
196
-
197
-(defmethod print-object ((object item) stream)
198
-  (print-unreadable-object (object stream :type t :identity t)
199
-    (with-slots (title link date) object
200
-      (format stream "title: ~s link: ~s date:~s"
201
-              (aif title (shorten-link it) "<untitled>")
202
-              (aif link (shorten-link it) "<no link>")
203
-              (aif date (shorten-link it) "<no date>")))))
204
-
205
-;{{{ RSS feed handling
206
-(defmethod make-item (xml-dom (type (eql :rss)))
207
-  (let* ((item-title ($ "> title" (text) (node)))
208
-         (item-link ($ "> link" (text) (node)))
209
-         (item-date ($ "> pubDate" (text) (node)))
210
-         (item-guid ($ "> guid" (text) (node)))
211
-         (item-description ($ "> description" (text) (node)))
212
-         (item-content-encoded ($ "> content::encoded" (text) (node)))
213
-         (content (with-output-to-string (s)
214
-                    (serialize (parse (or item-content-encoded item-description)) s)))
215
-         (*tag-dispatchers* *html-tags*))
216
-    (make-instance 'item
217
-                   :content content   
218
-                   :date item-date
219
-                   :doc xml-dom
220
-                   :id item-guid
221
-                   :link item-link
222
-                   :title item-title)))
223
-
224
-(defmethod %to-feed (xml-dom (type (eql :rss)) &key feed-link)
225
-  ; TODO: store feed-link
226
-  (lquery:initialize xml-dom)
227
-  (let ((doc-title ($ "channel > title" (text) (node)))
228
-        (doc-link ($ "channel > link" (text) (node)))
229
-        (doc-feed-link (or feed-link
230
-                           ($ "feed > atom::link[rel=self]" (first) (attr "href") (node)))))
231
-    (make-instance 'rss-feed :title doc-title :link doc-link :feed-link doc-feed-link)))
232
-;}}} 
233
-
234
-; {{{ ATOM feed handling
235
-
236
-
237 166
 (defun rdf-to-feed (xml-dom))
238 167
 (defun json-to-feed (json-object))
239 168
 (defun html5-to-feed (html-dom))
... ...
@@ -18,12 +18,13 @@
18 18
   (make-instance 'atom-person :name name :uri uri :email email))
19 19
 
20 20
 (defclass atom-feed (alimenta:feed)
21
-  ((subtitle   :initarg :subtitle                                 :initform nil)
22
-   (id         :initarg :id                                       :initform nil)
23
-   (icon       :initarg :icon                                     :initform nil)
21
+  ((subtitle   :initarg :subtitle                        :initform nil)
22
+   (id         :initarg :id                              :initform nil)
23
+   (icon       :initarg :icon                            :initform nil)
24 24
    (categories :initarg :categories :type (or null list) :initform nil)
25
-   (logo       :initarg :logo                                     :initform nil)
26
-   (authors    :initarg :authors    :type (or null list)   :initform nil)))
25
+   (logo       :initarg :logo                            :initform nil)
26
+   (updated    :initarg :updated                         :initform nil)
27
+   (authors    :initarg :authors    :type (or null list) :initform nil)))
27 28
 
28 29
 (defclass atom-item (alimenta:item)
29 30
   ((author-uri :initarg :author-uri :initform nil)))
... ...
@@ -64,7 +65,7 @@
64 65
                     (awhen (or item-content item-description) (serialize  (parse it) s)))))
65 66
     (make-instance 'atom-item
66 67
                    :content content
67
-                   :date item-date
68
+                   :date (local-time:parse-timestring item-date)
68 69
                    :id item-guid
69 70
                    :author item-author
70 71
                    :author-uri item-author-uri
... ...
@@ -90,6 +91,7 @@
90 91
           (doc-icon (get-feed-elem "feed > icon"))
91 92
           (doc-logo (get-feed-elem "feed > logo"))
92 93
           (doc-id (get-feed-elem "feed > id"))
94
+          (doc-updated (awhen (get-feed-elem "feed > updated") (local-time:parse-timestring it)))
93 95
           (doc-link (get-feed-elem-attr "feed > link[rel=alternate]" "href"))
94 96
           (doc-feed-link (or feed-link (get-feed-elem-attr "feed > link[rel=self]" "href")))
95 97
           (doc-categories ($ (inline xml-dom) "feed > category"
... ...
@@ -102,6 +104,7 @@
102 104
         :icon doc-icon
103 105
         :logo doc-logo
104 106
         :link doc-link
107
+        :updated doc-updated
105 108
         :id doc-id
106 109
         :feed-link doc-feed-link
107 110
         :subtitle doc-subtitle
... ...
@@ -134,7 +137,7 @@
134 137
                     (plump:make-element (plump:make-root) "feed"))))
135 138
     (prog1 parent
136 139
       (let ((feed-root (make-element parent "feed")))
137
-        (with-slots (title id link feed-link description) feed
140
+        (with-slots (title id updated link feed-link description) feed
138 141
           ($ (inline (make-element feed-root "title")) (text title)
139 142
 
140 143
              (inline (make-element feed-root "link"))
... ...
@@ -145,6 +148,7 @@
145 148
 
146 149
              (inline (make-element feed-root "id")) (text id) (node)
147 150
              (inline (make-element feed-root "summary")) (text description) (node)
151
+             (inline (make-element feed-root "updated")) (text updated) (node)
148 152
              ))))))
149 153
 
150 154
 
... ...
@@ -161,7 +165,9 @@
161 165
              (inline (make-element item-root "author"))
162 166
              (append ($ (inline (make-element item-root "name")) (text author)))
163 167
              (append ($ (inline (make-element item-root "uri"))  (text author-uri)))
164
-             (inline (make-element item-root "content")) (text content)))))))
168
+             (inline (make-element item-root "content")) (text content)
169
+             (inline (make-element item-root "updated")) (text date) (node)
170
+             ))))))
165 171
 
166 172
 
167 173
 (defconstants-really
... ...
@@ -6,6 +6,9 @@
6 6
            #:feed #:title #:link #:items #:feed-link #:doc #:source-type #:id #:date #:content
7 7
            #:item #:description #:%generate-xml #:%to-feed #:%get-items #:make-item))
8 8
 
9
+(defpackage #:alimenta.rss
10
+  (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta))
11
+
9 12
 (defpackage #:alimenta.atom
10 13
   (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta))
11 14
 
12 15
new file mode 100644
... ...
@@ -0,0 +1,73 @@
1
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
2
+
3
+(in-package :alimenta.rss)
4
+
5
+(defclass rss-feed (feed) ())
6
+(defclass rss-item (feed) ())
7
+
8
+(defmethod %get-items (xml-dom (feed-type (eql :rss)))
9
+  ($ (inline xml-dom) "channel > item"))
10
+
11
+(defmethod %generate-xml ((item item) (feed-type (eql :rss)) &key partial)
12
+  (prog1 partial
13
+    (let ((item-root (make-element ($ (inline partial) "channel" (node)) "item")))
14
+      (with-slots (title id date link content) item
15
+        ($ (inline (make-element item-root "title")) (text title)) 
16
+        ($ (inline (make-element item-root "link")) (text link)) 
17
+        (plump-dom:set-attribute
18
+          ($ (inline (make-element item-root "guid")) (text id) (node))
19
+          "isPermaLink"
20
+          "false") 
21
+        ($ (inline (make-element item-root "pubDate")) (text date)) 
22
+        ($ (inline (make-element item-root "description")) (text content))))))
23
+
24
+(defmethod %generate-xml ((feed feed) (feed-type (eql :rss)) &rest r)
25
+  (declare (ignore r))
26
+  (let* ((xml-root (plump:make-root))
27
+         (feed-root (plump:make-element xml-root "rss"))
28
+         (channel (plump-dom:make-element feed-root "channel")))
29
+    ($ (inline feed-root)
30
+       (attr "version" "2.0")
31
+       (attr "xmlns:content" "http://purl.org/rss/1.0/modules/content/")
32
+       (attr "xmlns:atom" "http://www.w3.org/2005/Atom"))
33
+    (with-slots (title link feed-link description) feed
34
+      ($ (inline (make-element channel "title"))
35
+         (text title))
36
+      ($ (inline (make-element channel "link"))
37
+         (text link))
38
+      (awhen description
39
+        ($ (inline (make-element channel "description"))
40
+           (text it)))
41
+      ($ (inline (make-element channel "atom:link"))
42
+         (attr "rel" "self")
43
+         (attr "type" "application/rss+xml")
44
+         (attr "href" link)))
45
+    xml-root))
46
+
47
+(defmethod make-item (xml-dom (type (eql :rss)))
48
+  (let* ((item-title ($ "> title" (text) (node)))
49
+         (item-link ($ "> link" (text) (node)))
50
+         (item-date ($ "> pubDate" (text) (node)))
51
+         (item-guid ($ "> guid" (text) (node)))
52
+         (item-description ($ "> description" (text) (node)))
53
+         (item-content-encoded ($ "> content::encoded" (text) (node)))
54
+         (content (with-output-to-string (s)
55
+                    (serialize (parse (or item-content-encoded item-description)) s)))
56
+         (*tag-dispatchers* *html-tags*))
57
+    (make-instance 'item
58
+                   :content content   
59
+                   :date item-date
60
+                   :doc xml-dom
61
+                   :id item-guid
62
+                   :link item-link
63
+                   :title item-title)))
64
+
65
+(defmethod %to-feed (xml-dom (type (eql :rss)) &key feed-link)
66
+  ; TODO: store feed-link
67
+  (lquery:initialize xml-dom)
68
+  (let ((doc-title ($ "channel > title" (text) (node)))
69
+        (doc-link ($ "channel > link" (text) (node)))
70
+        (doc-feed-link (or feed-link
71
+                           ($ "feed > atom::link[rel=self]" (first) (attr "href") (node)))))
72
+    (make-instance 'rss-feed :title doc-title :link doc-link :feed-link doc-feed-link)))
73
+
... ...
@@ -4,20 +4,6 @@
4 4
 (ql:quickload :spinneret)
5 5
 (ql:quickload :lass)
6 6
 
7
-(defmethod araneus:view ((name (eql 'root)) (item alimenta:item))
8
-  (with-slots ((title alimenta:title) (link alimenta:link)) item
9
-    (spinneret:with-html 
10
-      (:article
11
-        (:div.title title)
12
-        (:a.link :href link link)))))
13
-
14
-(defmethod araneus:view ((name (eql 'root)) (feed alimenta:feed))
15
-  (with-slots ((title alimenta:title) (link alimenta:link)) feed
16
-    (spinneret:with-html
17
-      (:header
18
-        (:h1.feed-title title)
19
-        (:a.feed-link link)))))
20
-
21 7
 (defun get-css ()
22 8
   (lass:compile-and-write
23 9
     `(*
... ...
@@ -45,6 +31,20 @@
45 31
        )
46 32
     ))
47 33
 
34
+ (defmethod araneus:view ((name (eql 'root)) (item alimenta:item))
35
+  (with-slots ((title alimenta:title) (link alimenta:link)) item
36
+    (spinneret:with-html 
37
+      (:article
38
+        (:div.title title)
39
+        (:a.link :href link link)))))
40
+
41
+(defmethod araneus:view ((name (eql 'root)) (feed alimenta:feed))
42
+  (with-slots ((title alimenta:title) (link alimenta:link)) feed
43
+    (spinneret:with-html
44
+      (:header
45
+        (:h1.feed-title title)
46
+        (:a.feed-link link)))))
47
+
48 48
 (defmethod araneus:view :around ((name (eql 'root)) (feed alimenta:feed))
49 49
   (with-slots ((title alimenta:title) (items alimenta::items)) feed
50 50
     (spinneret:with-html-string