git.fiddlerwoaroof.com
Browse code

Finishing atom support, testing server.

- Added more complete support of the Atom suspect

- TODO: support dates !!!

- Added a test web-app

fiddlerwoaroof authored on 08/02/2016 18:20:05
Showing 5 changed files
... ...
@@ -13,6 +13,7 @@
13 13
   :serial t
14 14
   :components ((:file "package")
15 15
                (:file "alimenta")  
16
+               (:file "atom")  
16 17
                (:file "fetching")
17 18
                (:file "discovery")))
18 19
 
... ...
@@ -24,19 +24,11 @@
24 24
 
25 25
 (defgeneric %to-feed (doc type &key feed-link))
26 26
 
27
-(defgeneric get-items (feed doc type)
28
-  (:documentation
29
-    "Get items for a feed. Specialize a symbol on type, to get items from a new sort of feed"))
30
-
31 27
 (defgeneric %generate-xml (feed feed-type &key partial))
32 28
 
33 29
 (defun generate-xml (feed &key (feed-type :rss))
34 30
   (%generate-xml feed feed-type))
35 31
 
36
-(defmethod %generate-xml :around (feed feed-type &key partial)
37
-  (call-next-method feed feed-type :partial partial))
38
-
39
-
40 32
 (defun to-feed (doc &key type feed-link)
41 33
   "Makes an instance of feed from the given document.  Specialize %to-feed with
42 34
    an equal-specializer on type with an appropriate symbol to implement a new
... ...
@@ -45,9 +37,12 @@
45 37
     (setf type (detect-feed-type doc)))
46 38
   (%to-feed doc type :feed-link feed-link))
47 39
 
40
+(defgeneric %get-items (xml feed-type)
41
+  (:method (xml-dom (feed-type (eql :rss))) ($ (inline xml-dom) "channel > item")))
42
+
48 43
 (defun get-items (feed xml-dom &key type)
49 44
   (with-slots (items) feed
50
-    (loop for item across ($ (inline xml-dom) "channel > item")
45
+    (loop for item across (%get-items xml-dom type)
51 46
           do (push (make-item xml-dom type) items)
52 47
           finally (return items)))) 
53 48
 
... ...
@@ -57,8 +52,11 @@
57 52
   (aprog1 (call-next-method xml-dom doc-type :feed-link feed-link)
58 53
     (with-slots (doc source-type) it
59 54
       (setf doc xml-dom
60
-            source-type feed-link))
61
-    (get-items it xml-dom :type doc-type)))
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))))))
62 60
 
63 61
 (defgeneric feed-to-rss (feed))
64 62
 (defgeneric feed-to-atom (feed))
... ...
@@ -126,7 +124,9 @@
126 124
               (aif title (shorten-link it) "<untitled>")
127 125
               (aif link (shorten-link it) "<no link>")))))
128 126
 
129
-(defmethod %generate-xml ((feed feed) (feed-type (eql :rss)) &rest r)
127
+(defclass rss-feed (feed) ())
128
+
129
+(defmethod %generate-xml ((feed rss-feed) (feed-type (eql :rss)) &rest r)
130 130
   (declare (ignore r))
131 131
   (let* ((xml-root (plump:make-root))
132 132
          (feed-root (plump:make-element xml-root "rss"))
... ...
@@ -152,11 +152,35 @@
152 152
 (defclass item ()
153 153
   ((title :initarg :title :initform nil)
154 154
    (id :initarg :id :initform nil)
155
+   (author :initarg :author :initform nil)
155 156
    (date :initarg :date :initform nil)
156 157
    (link :initarg :link :initform nil)
158
+   (links :initform (make-hash-table :test #'equalp))
157 159
    (content :initarg :content :initform nil)
158 160
    (doc :initarg :doc :initform nil)))
159 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
+
160 184
 (defmethod %generate-xml ((item item) (feed-type (eql :rss)) &key partial)
161 185
   (prog1 partial
162 186
     (let ((item-root (make-element ($ (inline partial) "channel" (node)) "item")))
... ...
@@ -204,39 +228,12 @@
204 228
         (doc-link ($ "channel > link" (text) (node)))
205 229
         (doc-feed-link (or feed-link
206 230
                            ($ "feed > atom::link[rel=self]" (first) (attr "href") (node)))))
207
-    (make-instance 'feed :title doc-title :link doc-link :feed-link doc-feed-link)))
231
+    (make-instance 'rss-feed :title doc-title :link doc-link :feed-link doc-feed-link)))
208 232
 ;}}} 
209 233
 
210 234
 ; {{{ ATOM feed handling
211
-(defmethod make-item (xml-dom (type (eql :atom)))
212
-  (let* ((item-title ($ "> title" (text) (node)))
213
-         (item-link ($ "> link[rel=alternate]" (attr "href") (first) (node)))
214
-         (item-date (or ($ "> updated" (text) (node))
215
-                        ($ "> published" (text) (node)))) ;; Which should be default?
216
-         (item-guid ($ "> id" (text) (node)))
217
-         (item-description ($ "> summary" (text) (node)))
218
-         (item-content ($ "> content" (text) (node)))
219
-         (*tag-dispatchers* *html-tags*) 
220
-         (content (with-output-to-string (s)
221
-                    (serialize (parse (or item-content item-description)) s))))
222
-    (make-instance 'item
223
-                   :content content   
224
-                   :date item-date 
225
-                   :id item-guid 
226
-                   :link item-link 
227
-                   :title item-title)))
228 235
 
229
-(defmethod %to-feed (xml-dom (type (eql :atom)) &key feed-link)
230
-  (declare (ignore type) (ignorable feed-link))
231
-  ; TODO: store feed-link
232
-  (lquery:initialize xml-dom)
233
-  (let ((doc-title ($ "feed > title" (text) (node)))
234
-        (doc-link ($ "feed > link[rel=alternate]" (first) (attr "href") (node)))
235
-        (doc-feed-link (or feed-link
236
-                           ($ "feed > link[rel=self]" (first) (attr "href") (node)))))
237
-    (make-instance 'feed :title doc-title :link doc-link :feed-link doc-feed-link)))
238
-;}}}
239
-  
236
+
240 237
 (defun rdf-to-feed (xml-dom))
241 238
 (defun json-to-feed (json-object))
242 239
 (defun html5-to-feed (html-dom))
243 240
new file mode 100644
... ...
@@ -0,0 +1,317 @@
1
+(declaim (optimize (debug 3) (safety 3) (speed 0)))
2
+(in-package :alimenta.atom)
3
+
4
+(defclass atom-category ()
5
+  ((term :initarg :term :initform nil)
6
+   (label :initarg :label :initform nil)
7
+   (scheme :initarg :scheme :initform nil)))
8
+
9
+(defun make-category (term &optional label scheme)
10
+  (make-instance 'atom-category :term term :label label :scheme scheme))
11
+
12
+(defclass atom-person ()
13
+  ((name  :initarg :name  :type (or null string) :initform nil)
14
+   (uri   :initarg :uri   :type (or null string) :initform nil)
15
+   (email :initarg :email :type (or null string) :initform nil)))
16
+
17
+(defun make-person (name &optional uri email)
18
+  (make-instance 'atom-person :name name :uri uri :email email))
19
+
20
+(defclass atom-feed (alimenta:feed)
21
+  ((subtitle   :initarg :subtitle                                 :initform nil)
22
+   (id         :initarg :id                                       :initform nil)
23
+   (icon       :initarg :icon                                     :initform nil)
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)))
27
+
28
+(defclass atom-item (alimenta:item)
29
+  ((author-uri :initarg :author-uri :initform nil)))
30
+
31
+(defmethod alimenta::%get-items (xml-dom (feed-type (eql :atom)))
32
+  ($ (inline xml-dom) "feed > entry"))
33
+
34
+(defclass alimenta::link ()
35
+  ((alimenta::relation :initarg :rel)
36
+   (alimenta::target   :initarg :target)))
37
+
38
+(defun get-link (xml)
39
+  "This only handles alternate links"
40
+  (let ((links ($ (inline xml) "> link[rel=alternate]" (combine (attr :type) (attr :href)))))
41
+    (map 'list
42
+         (lambda (x)
43
+           (destructuring-bind (type href) x
44
+             (setf (alimenta::link (make-keyword (string-upcase type)))
45
+                   (cons type href))))
46
+         links)))
47
+
48
+(defmethod make-item (xml-dom (type (eql :atom)))
49
+  (let* ((lquery:*lquery-master-document* xml-dom)
50
+         (item-title ($ "> title" (text) (node)))
51
+         (links ($ "> link" (combine (attr "rel") (attr "href"))))
52
+         (sel-links (cadr (find-if (lambda (x) (aif (car x) (equal it "alternate") t))
53
+                                   links)))
54
+         (item-link (or sel-links (cdr (when (> (length links) 0) (elt links 0)))))
55
+         (item-date (or ($ "> updated" (text) (node))
56
+                        ($ "> published" (text) (node)))) ;; Which should be default?
57
+         (item-guid ($ "> id" (text) (node)))
58
+         (item-description ($ "> summary" (text) (node)))
59
+         (item-content ($ "> content" (text) (node)))
60
+         (item-author ($ "> author > name" (text) (node)))
61
+         (item-author-uri ($ "> author > uri" (text) (node)))
62
+         (*tag-dispatchers* *html-tags*)
63
+         (content (with-output-to-string (s)
64
+                    (awhen (or item-content item-description) (serialize  (parse it) s)))))
65
+    (make-instance 'atom-item
66
+                   :content content
67
+                   :date item-date
68
+                   :id item-guid
69
+                   :author item-author
70
+                   :author-uri item-author-uri
71
+                   :link item-link
72
+                   :title item-title)))
73
+
74
+(defun get-authors (xml-dom)
75
+  (let ((authors ($ (inline xml-dom) "feed > author")))
76
+    (loop for author across authors
77
+          collect (make-person
78
+                    ($ (inline author) "> name" (text) (node))
79
+                    ($ (inline author) "> uri" (text) (node))
80
+                    ($ (inline author) "> email" (text) (node))))))
81
+
82
+(defmethod %to-feed (xml-dom (type (eql :atom)) &key feed-link)
83
+  (declare (ignore type) (ignorable feed-link))
84
+  ; TODO: store feed-link
85
+  (flet ((get-feed-elem (selector) ($ (inline xml-dom) selector (text) (node)))
86
+         (get-feed-elem-attr (selector attr) ($ (inline xml-dom) selector (attr attr) (node))))
87
+    (let ((doc-title (get-feed-elem "feed > title"))
88
+          (doc-subtitle (get-feed-elem "feed > subtitle"))
89
+          (doc-summary (get-feed-elem "feed > summary"))
90
+          (doc-icon (get-feed-elem "feed > icon"))
91
+          (doc-logo (get-feed-elem "feed > logo"))
92
+          (doc-id (get-feed-elem "feed > id"))
93
+          (doc-link (get-feed-elem-attr "feed > link[rel=alternate]" "href"))
94
+          (doc-feed-link (or feed-link (get-feed-elem-attr "feed > link[rel=self]" "href")))
95
+          (doc-categories ($ (inline xml-dom) "feed > category"
96
+                             (combine (attr "term") (attr "label") (attr "scheme"))
97
+                             (map-apply #'make-category)))
98
+          (doc-authors (get-authors xml-dom)))
99
+      (make-instance 'atom-feed
100
+        :title doc-title
101
+        :description doc-summary
102
+        :icon doc-icon
103
+        :logo doc-logo
104
+        :link doc-link
105
+        :id doc-id
106
+        :feed-link doc-feed-link
107
+        :subtitle doc-subtitle
108
+        :categories (coerce doc-categories 'list)
109
+        :authors doc-authors
110
+        ))))
111
+;}}}
112
+
113
+(defmacro defconstants (&body constants)
114
+  (list*
115
+    'progn
116
+    (loop for (name value &optional doc) in constants
117
+        collect `(defconstant ,name ,value ,doc))))
118
+
119
+(defvar *defconstants-really-verbose* nil)
120
+(defmacro defconstants-really (&body constants)
121
+  "auto-invoke the continue restart . . ."
122
+  `(handler-bind ((sb-ext:defconstant-uneql
123
+                    (lambda (c)
124
+                      (when *defconstants-really-verbose*
125
+                        (format t "~&Changing definition of ~s from ~s to ~s~%"
126
+                                (sb-ext:defconstant-uneql-name c)
127
+                                (sb-ext:defconstant-uneql-old-value c)
128
+                                (sb-ext:defconstant-uneql-new-value c)))
129
+                      (continue c))))
130
+     (defconstants ,@constants)))
131
+
132
+(defmethod %generate-xml ((feed feed) (feed-type (eql :atom)) &key partial)
133
+  (let ((parent (or ($ (inline partial) "feed" (node))
134
+                    (plump:make-element (plump:make-root) "feed"))))
135
+    (prog1 parent
136
+      (let ((feed-root (make-element parent "feed")))
137
+        (with-slots (title id link feed-link description) feed
138
+          ($ (inline (make-element feed-root "title")) (text title)
139
+
140
+             (inline (make-element feed-root "link"))
141
+             (attr "href" feed-link) (attr "rel" "self")
142
+
143
+             (inline (make-element feed-root "link"))
144
+             (attr "href" link) (attr "rel" "alternate") (attr "type" "text/html")
145
+
146
+             (inline (make-element feed-root "id")) (text id) (node)
147
+             (inline (make-element feed-root "summary")) (text description) (node)
148
+             ))))))
149
+
150
+
151
+(defmethod %generate-xml ((item item) (feed-type (eql :atom)) &key partial)
152
+  (let ((parent (or ($ (inline partial) "feed" (node))
153
+                    (plump:make-element (plump:make-root) "feed"))))
154
+    (prog1 parent
155
+      (let ((item-root (make-element parent "entry")))
156
+        (with-slots (title id date link content (author alimenta::author) author-uri) item
157
+          ($ (inline (make-element item-root "title")) (text title)
158
+             (inline (make-element item-root "link")) (attr "href" link)
159
+             (inline (make-element item-root "id")) (text id) (node)
160
+             (inline (make-element item-root "pubDate")) (text date)
161
+             (inline (make-element item-root "author"))
162
+             (append ($ (inline (make-element item-root "name")) (text author)))
163
+             (append ($ (inline (make-element item-root "uri"))  (text author-uri)))
164
+             (inline (make-element item-root "content")) (text content)))))))
165
+
166
+
167
+(defconstants-really
168
+  (+title+ "The Title")
169
+  (+author+ "Joe Q Public")
170
+  (+author-uri+ "http://example.com/joeq")
171
+  (+content+ "Teh Content")
172
+  (+id+ "t3_43tjwv")
173
+  (+link+ "http://example.com/something")
174
+  (+published+ "2016-02-02T09:41:27+00:00")
175
+
176
+  (+entry1+
177
+    (format nil
178
+            "<entry>
179
+             <author>
180
+             <name>~A</name>
181
+             <uri>~A</uri>
182
+             </author>
183
+             <category term='programming' label='/r/programming'/>
184
+             <content type='html'>~A</content>
185
+             <id>~a</id>
186
+             <link href='~a'/>
187
+             <published>~a</published>
188
+             <title>~a</title>
189
+             </entry>"
190
+            +author+ +author-uri+ +content+ +id+ +link+
191
+            +published+ +title+))
192
+
193
+    (+feed-category-term+ "testing")
194
+    (+feed-category-label+ "/r/testing")
195
+    (+feed-id+ "The Feed")
196
+    (+feed-icon+ "http://example.com/feed.png")
197
+    (+feed-logo+ "http://example.com/logo.png")
198
+    (+feed-link-website+ "http://example.com")
199
+    (+feed-link-self+ "http://example.com/atom.xml")
200
+    (+feed-subtitle+ "The SubTitle")
201
+    (+feed-title+ "The Title")
202
+    (+feed-author-name+ "The Author")
203
+    (+feed-author-uri+ "http://example.com/theauthor")
204
+    (+feed-description+ "The description")
205
+    (+feed1+
206
+      (format nil
207
+              "<feed>
208
+               <title>~a</title>
209
+               <subtitle>~a</subtitle>
210
+               <icon>~a</icon>
211
+               <category term=\"~a\" label=\"~a\"/>
212
+               <link rel=\"alternate\" href=\"~a\" type=\"text/html\" />
213
+               <link rel=\"self\" href=\"~a\" />
214
+               <logo>~a</logo>
215
+               <summary>~a</summary>
216
+               <author><name>~a</name><uri>~a</uri></author>
217
+               <author><name>~a</name><uri>~a</uri></author>
218
+               <id>~a</id>
219
+               </feed>"
220
+              +feed-title+
221
+              +feed-subtitle+
222
+              +feed-icon+
223
+              +feed-category-term+ +feed-category-label+
224
+              +feed-link-website+
225
+              +feed-link-self+
226
+              +feed-logo+
227
+              +feed-description+
228
+              +feed-author-name+ +feed-author-uri+
229
+              +feed-author-name+ +feed-author-uri+
230
+              +feed-id+
231
+              )))
232
+
233
+(defun true (x) (not (null x)))
234
+
235
+(defun get-node-text (xml-doc selector)
236
+  ($ (inline xml-doc) selector (text) (node)))
237
+
238
+(deftest to-feed ()
239
+  (let ((xml (parse +feed1+)))
240
+    (symbol-macrolet ((feed (alimenta::%to-feed xml :atom)))
241
+      (should be equal +feed-title+ (slot-value feed 'alimenta:title))
242
+      (should be equal +feed-link-website+ (slot-value feed 'alimenta:link))
243
+      (should be equal +feed-link-self+ (slot-value feed 'alimenta:feed-link))
244
+      (should be equal +feed-description+ (slot-value feed 'description))
245
+      (should be equal +feed-id+ (slot-value feed 'id))
246
+      (should be equal +feed-subtitle+ (slot-value feed 'subtitle))
247
+      (should be equal +feed-icon+ (slot-value feed 'icon))
248
+      (should be equal +feed-logo+ (slot-value feed 'logo))
249
+
250
+      (should be equal +feed-category-term+
251
+              (slot-value
252
+                (elt 
253
+                  (slot-value feed 'categories)
254
+                  0)
255
+                'term))
256
+      (should be equal +feed-category-label+
257
+              (slot-value
258
+                (elt
259
+                  (slot-value feed 'categories)
260
+                  0)
261
+                'label))
262
+
263
+      (should be equal +feed-author-name+
264
+              (slot-value
265
+                (elt (slot-value feed 'authors) 0)
266
+                'name))
267
+      (should be equal +feed-author-uri+
268
+              (slot-value
269
+                (elt (slot-value feed 'authors) 0)
270
+                'uri))
271
+
272
+      ;(should be equal +feed-title+ (slot-value feed 'alimenta:title))
273
+      ;(should be equal +feed-title+ (slot-value feed 'alimenta:title))
274
+      ;(should be equal +feed-title+ (slot-value feed 'alimenta:title))
275
+      )
276
+    )
277
+  )
278
+
279
+(deftest make-item ()
280
+  (let ((xml (lquery:$ (inline (plump:parse +entry1+)) "entry" (node))))
281
+    (symbol-macrolet ((item (alimenta::make-item xml :atom)))
282
+      (should be true item)
283
+      (should be equal +link+ (slot-value item 'alimenta:link))
284
+      (should be equal +content+ (slot-value item 'alimenta:content))
285
+      (should be equal +author+ (slot-value item 'alimenta::author))
286
+      (should be equal +author-uri+ (slot-value item 'author-uri))
287
+      (should be equal +id+ (slot-value item 'alimenta:id)))))
288
+
289
+(defparameter *tmp* nil)
290
+(deftest generate-xml ()
291
+  (let* ((xml ($ (inline (parse +entry1+)) "entry" (node)))
292
+         (item (alimenta::make-item xml :atom)))
293
+    (symbol-macrolet ((generated-xml (alimenta::%generate-xml item :atom)))
294
+      (should be equal +title+
295
+              ($ (inline generated-xml) "entry > title" (text) (node)))
296
+      (should be equal +author+
297
+              ($ (inline generated-xml) "entry > author > name" (text) (node)))
298
+      (should be equal +author-uri+
299
+              ($ (inline generated-xml) "entry > author > uri" (text) (node)))
300
+      (should be equal +id+
301
+              ($ (inline generated-xml) "entry > id" (text) (node)))
302
+      (should be equal +content+
303
+              ($ (inline generated-xml) "entry > content" (text) (node)))
304
+      (should be equal +link+
305
+              ($ (inline generated-xml) "entry > link" (attr "href") (node)))
306
+      ; TODO: deal with dates . . .
307
+      )))
308
+
309
+(defun do-test (&optional (test nil))
310
+  (let ((st:*test-output* *debug-io*))
311
+    (multiple-value-bind (result hm? errors) (st:test :test test)
312
+      (format t
313
+              "~&Returns: ~a~%Error:~%~{~a~^~%~}~%Failures-vals:~%~{~a~^ ~}~%"
314
+              result
315
+              errors
316
+              hm?
317
+              ))))
... ...
@@ -4,7 +4,10 @@
4 4
   (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora)
5 5
   (:export #:to-feed #:generate-xml
6 6
            #:feed #:title #:link #:items #:feed-link #:doc #:source-type #:id #:date #:content
7
-           #:item))
7
+           #:item #:description #:%generate-xml #:%to-feed #:%get-items #:make-item))
8
+
9
+(defpackage #:alimenta.atom
10
+  (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta))
8 11
 
9 12
 (defpackage #:alimenta.discover
10 13
   (:use #:cl #:alimenta #:alexandria #:anaphora #:lquery)
... ...
@@ -18,3 +21,17 @@
18 21
   (asdf:load-system :alimenta)
19 22
   (st:test :package :alimenta)
20 23
   t)
24
+
25
+(defpackage #:alimenta.test-runner
26
+  (:use #:cl #:alimenta #:alimenta.atom #:alimenta.discover #:alimenta.pull-feed))
27
+
28
+(in-package :alimenta.test-runner)
29
+
30
+(defclass xunit-test (asdf:test-op) ())
31
+
32
+(defmethod asdf:perform ((o asdf:test-op) (s (eql (asdf:find-system :alimenta))))
33
+  (asdf:load-system :alimenta)
34
+  (or (st:test :package :alimenta)
35
+      (st:test :package :alimenta.atom)
36
+      (st:test :package :alimenta.discover)
37
+      (st:test :package :alimenta.pull-feed)))
21 38
new file mode 100644
... ...
@@ -0,0 +1,72 @@
1
+(ql:quickload :clack)
2
+(ql:quickload :ningle )
3
+(ql:quickload :araneus)
4
+(ql:quickload :spinneret)
5
+(ql:quickload :lass)
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
+(defun get-css ()
22
+  (lass:compile-and-write
23
+    `(*
24
+       :padding "0px"
25
+       :margin "0px")
26
+    `(body
27
+       :box-sizing "border-box"
28
+       :font-family sans-serif
29
+       )
30
+
31
+    `(header
32
+       :border-bottom "thin solid black"
33
+       :width "100%"
34
+       :text-align center
35
+       :margin-bottom "2em"
36
+       :padding "1em")
37
+
38
+    `(article
39
+       :padding "1em"
40
+       :border "4px double #888"
41
+       :display "inline-block"
42
+       :width "30%"
43
+       :overflow "hidden"
44
+       :min-height "4em"
45
+       )
46
+    ))
47
+
48
+(defmethod araneus:view :around ((name (eql 'root)) (feed alimenta:feed))
49
+  (with-slots ((title alimenta:title) (items alimenta::items)) feed
50
+    (spinneret:with-html-string
51
+      (:html
52
+        (:head (:title title))
53
+        (:style
54
+          :type "text/css"
55
+          (get-css))
56
+        (:body
57
+          (:main
58
+            (call-next-method)
59
+            (loop for item in items
60
+                  do (araneus:view 'root item))))))))
61
+
62
+(araneus:define-controller root (params)
63
+  (let* ((url "https://reddit.com/r/programming.rss")
64
+         (feed (alimenta.pull-feed::fetch-doc-from-url url)))
65
+    (alimenta:to-feed feed :type :atom :feed-link url)))
66
+
67
+(defvar *app* (make-instance 'ningle:<app>))
68
+
69
+(araneus:defroutes *app*
70
+  (("/") (araneus:as-route 'root)))
71
+
72
+(defvar *handler* (clack:clackup *app* :port 4939))