Browse code
variou structural changes
fiddlerwoaroof authored on 19/07/2016 08:49:26
Showing 6 changed files
Showing 6 changed files
... | ... |
@@ -3,8 +3,18 @@ A library for handling RSS/Atom feeds |
3 | 3 |
The library implements most of the elements in the RSS and Atom specs and handles feed discovery, feed type |
4 | 4 |
detection and feed pulling as well as feed generation. It is also fairly easy to extend by subclassing the |
5 | 5 |
appropriate classes and specializing the generic functions `%to-feed`, `make-item`, `%get-items` and |
6 |
-`%generate-xml`. |
|
6 |
+`generate-xml`. |
|
7 | 7 |
|
8 | 8 |
It is, however, noticeably incomplete: particularly, not all the useful functions have been exported from the |
9 |
-packages they reside in. Also, it relies on code in my fork of chronicity <http://github.com/fiddlerwoaroof/chronicity> |
|
10 |
-to handle certain wordpress feeds. |
|
9 |
+packages they reside in. Also, it relies on code in my fork of chronicity |
|
10 |
+<http://github.com/fiddlerwoaroof/chronicity> to handle certain wordpress feeds. |
|
11 |
+ |
|
12 |
+There are a couple additional features thrown in: |
|
13 |
+ |
|
14 |
+- server-test.lisp runs a demo web server that pulls a bunch of feeds from reddit and serves them up as a |
|
15 |
+ single page website. It's main reason for existence is to test this library and the library I've been |
|
16 |
+ working on for web stuff <http://github.com/fiddlerwoaroof/araneus> |
|
17 |
+ |
|
18 |
+- alimenta-clim.lisp runs a demo graphical application that uses McCLIM |
|
19 |
+ <https://github.com/robert-strandh/mcclim> to generate an X application that can be used to navigate an RSS |
|
20 |
+ feed. |
... | ... |
@@ -1,39 +1,39 @@ |
1 | 1 |
;;;; alimenta.lisp |
2 |
-(declaim (optimize (speed 0) (safety 3) (debug 3))) |
|
2 |
+(declaim (optimize (speed 0) (safety 3) (debug 4))) |
|
3 | 3 |
|
4 | 4 |
(in-package #:alimenta) |
5 | 5 |
|
6 |
+(defgeneric -to-feed (doc type &key feed-link) |
|
7 |
+ (:documentation "Given an xml-document, return a feed object")) |
|
8 |
+ |
|
9 |
+(defgeneric generate-xml (feed feed-type &key partial) |
|
10 |
+ (:documentation "Given a lisp object representing a feed, return an xml |
|
11 |
+ document")) |
|
12 |
+ |
|
6 | 13 |
(defclass feed () |
7 |
- ((title :initarg :title :initform nil) |
|
8 |
- (link :initarg :link :initform nil) |
|
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) |
|
9 | 17 |
(items :initarg :items :initform nil :accessor items) |
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))) |
|
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))) |
|
14 | 21 |
|
15 | 22 |
(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)) |
|
23 |
+ ((author :initarg :author :initform nil :accessor author) |
|
22 | 24 |
(content :initarg :content :initform nil :accessor content) |
23 |
- (doc :initarg :doc :initform nil))) |
|
25 |
+ (date :initarg :date :initform nil :accessor date) |
|
26 |
+ (doc :initarg :doc :initform nil :accessor doc) |
|
27 |
+ (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))) |
|
24 | 31 |
|
25 | 32 |
(defclass complex-value () ()) |
26 | 33 |
|
27 | 34 |
(defgeneric primary-value (self) |
28 |
- (:documentation "Primarily for COMPLEX-VALUES: this should take one and return a useful primary value") |
|
29 |
- (:method ((self t)) self)) |
|
30 |
- |
|
31 |
-(define-condition duplicate-link-type (error) |
|
32 |
- ((old :reader duplicate-link-type-old :initarg :old) |
|
33 |
- (new :reader duplicate-link-type-new :initarg :new)) |
|
34 |
- (:report (lambda (condition stream) |
|
35 |
- (format stream "Item already has link ~s" (duplicate-link-type-old condition))))) |
|
36 |
- |
|
35 |
+ (:documentation "Primarily for COMPLEX-VALUES: this should take one and |
|
36 |
+ return a useful primary value")) |
|
37 | 37 |
|
38 | 38 |
(defgeneric push-item (feed item) |
39 | 39 |
(:documentation "Adds an item to the feed")) |
... | ... |
@@ -41,31 +41,40 @@ |
41 | 41 |
(defgeneric make-item (xml-dom doc-type) |
42 | 42 |
(:documentation "Given an xml document, return an item")) |
43 | 43 |
|
44 |
-(defgeneric parse-feed (feed)) |
|
44 |
+(defgeneric parse-feed (feed) |
|
45 |
+ (:documentation "Parse a feed into a lisp object")) |
|
46 |
+ |
|
47 |
+(defgeneric get-items (xml feed-type) |
|
48 |
+ (:documentation "Given an xml document, extract its items")) |
|
45 | 49 |
|
46 |
-(defgeneric %get-items (xml feed-type)) |
|
50 |
+(defmethod primary-value ((self t)) |
|
51 |
+ self) |
|
47 | 52 |
|
48 |
-(defgeneric %generate-xml (feed feed-type &key partial)) |
|
49 |
-(defmethod %generate-xml :around ((feed feed) feed-type &rest r) |
|
53 |
+(define-condition duplicate-link-type (error) |
|
54 |
+ ((old :reader duplicate-link-type-old :initarg :old) |
|
55 |
+ (new :reader duplicate-link-type-new :initarg :new)) |
|
56 |
+ (:report (lambda (condition stream) |
|
57 |
+ (format stream "Item already has link ~s" (duplicate-link-type-old condition))))) |
|
58 |
+ |
|
59 |
+ |
|
60 |
+(defmethod generate-xml :around ((feed feed) feed-type &rest r) |
|
50 | 61 |
(declare (ignore r)) |
51 | 62 |
(let ((result (call-next-method feed feed-type))) |
52 | 63 |
(with-slots (items) feed |
53 | 64 |
(loop for item in items |
54 |
- do (%generate-xml item feed-type :partial result))) |
|
65 |
+ do (generate-xml item feed-type :partial result))) |
|
55 | 66 |
result)) |
56 | 67 |
|
57 |
-(defgeneric %to-feed (doc type &key feed-link) |
|
58 |
- (:documentation "Given an xml-document, return a feed object")) |
|
59 |
-(defmethod %to-feed :around ((xml-dom plump:node) doc-type &key feed-link) |
|
60 |
- "This wraps the particular methods so that _they_ don't have to implement item fetching. |
|
61 |
- NIL passed to the type activates auto-detection" |
|
68 |
+(defmethod -to-feed :around ((xml-dom plump:node) doc-type &key feed-link) |
|
69 |
+ "This wraps the particular methods so that _they_ don't have to implement |
|
70 |
+ item fetching. NIL passed to the type activates auto-detection" |
|
62 | 71 |
(aprog1 (call-next-method xml-dom doc-type :feed-link feed-link) |
63 | 72 |
(with-slots (doc source-type) it |
64 | 73 |
(setf doc xml-dom |
65 | 74 |
source-type doc-type)) |
66 | 75 |
(with-slots (items) it |
67 | 76 |
(setf |
68 |
- items (loop for item across (%get-items xml-dom doc-type) |
|
77 |
+ items (loop for item across (get-items xml-dom doc-type) |
|
69 | 78 |
collect (make-item item doc-type)))))) |
70 | 79 |
|
71 | 80 |
(defgeneric (setf link) (value self)) |
... | ... |
@@ -109,50 +118,31 @@ |
109 | 118 |
(t root-node-name))))) |
110 | 119 |
|
111 | 120 |
|
112 |
-(defun generate-xml (feed &key (feed-type :rss)) |
|
113 |
- (%generate-xml feed feed-type)) |
|
121 |
+;(defun generate-xml (feed &key (feed-type :rss)) |
|
122 |
+; (%generate-xml feed feed-type)) |
|
114 | 123 |
|
115 | 124 |
(defun to-feed (doc &key type feed-link) |
116 |
- "Makes an instance of feed from the given document. Specialize %to-feed with |
|
125 |
+ "Makes an instance of feed from the given document. Specialize to-feed with |
|
117 | 126 |
an equal-specializer on type with an appropriate symbol to implement a new |
118 | 127 |
sort of feed." |
119 | 128 |
(unless type |
120 | 129 |
(setf type (detect-feed-type doc))) |
121 |
- (%to-feed doc type :feed-link feed-link)) |
|
122 |
- |
|
123 |
- |
|
124 |
-(defun get-items (feed xml-dom &key type) |
|
125 |
- (with-slots (items) feed |
|
126 |
- (loop for item across (%get-items xml-dom type) |
|
127 |
- do (push (make-item xml-dom type) items) |
|
128 |
- finally (return items)))) |
|
129 |
- |
|
130 |
-(defgeneric feed-to-rss (feed)) |
|
131 |
-(defgeneric feed-to-atom (feed)) |
|
132 |
-(defgeneric feed-to-json (feed)) |
|
133 |
-(defgeneric feed-to-html5 (feed) |
|
134 |
- (:documentation |
|
135 |
- "take a feed object, produce an html5 output. Simple format: |
|
136 |
- <!DOCTYPE html> |
|
137 |
- <html lang=\"en\"> |
|
138 |
- <head> |
|
139 |
- <meta charset=\"UTF-8\"> |
|
140 |
- <title>Feed Title</title> |
|
141 |
- </head> |
|
142 |
- <body> |
|
143 |
- <main> |
|
144 |
- <article id=\"id\"> |
|
145 |
- <h1>Title</h1> |
|
146 |
- <h2>Author</h2> |
|
147 |
- <span class=\"date\">Date</span> |
|
148 |
- <p>Content</p> |
|
149 |
- </article> |
|
150 |
- </main> |
|
151 |
- </body> |
|
152 |
- </html>")) |
|
130 |
+ (-to-feed doc type :feed-link feed-link)) |
|
131 |
+ |
|
132 |
+ |
|
133 |
+;(defun -get-items (feed xml-dom &key type) |
|
134 |
+; (with-slots (items) feed |
|
135 |
+; (loop for item across (get-items xml-dom type) |
|
136 |
+; do (push (make-item xml-dom type) items) |
|
137 |
+; finally (return items)))) |
|
153 | 138 |
|
154 | 139 |
(defun make-feed (&key title link items feed-link description) |
155 |
- (make-instance 'feed :title title :link link :items items :feed-link feed-link :description description)) |
|
140 |
+ (make-instance 'feed |
|
141 |
+ :description description |
|
142 |
+ :feed-link feed-link |
|
143 |
+ :items items |
|
144 |
+ :link link |
|
145 |
+ :title title)) |
|
156 | 146 |
|
157 | 147 |
(let ((n 0)) |
158 | 148 |
(defun next-id () |
... | ... |
@@ -169,10 +159,6 @@ |
169 | 159 |
(let ((link (cl-ppcre:regex-replace "^https?:" link ""))) |
170 | 160 |
(subseq link 0 (min 30 (length link))))) |
171 | 161 |
|
172 |
-(defun rdf-to-feed (xml-dom)) |
|
173 |
-(defun json-to-feed (json-object)) |
|
174 |
-(defun html5-to-feed (html-dom)) |
|
175 |
- |
|
176 | 162 |
(defmethod push-item ((feed feed) (item item)) |
177 | 163 |
(push item (slot-value feed 'items))) |
178 | 164 |
|
... | ... |
@@ -33,7 +33,7 @@ |
33 | 33 |
(defun make-person (name &optional uri email) |
34 | 34 |
(make-instance 'atom-person :name name :uri uri :email email)) |
35 | 35 |
|
36 |
-(defmethod alimenta::%get-items (xml-dom (feed-type (eql :atom))) |
|
36 |
+(defmethod alimenta::get-items (xml-dom (feed-type (eql :atom))) |
|
37 | 37 |
($ (inline xml-dom) "feed > entry")) |
38 | 38 |
|
39 | 39 |
(defun get-link (xml) |
... | ... |
@@ -64,7 +64,7 @@ |
64 | 64 |
(content (with-output-to-string (s) |
65 | 65 |
(awhen (or item-content item-description) (serialize (parse it) s))))) |
66 | 66 |
(make-instance 'atom-item |
67 |
- :doc xml-dom |
|
67 |
+ :doc xml-dom |
|
68 | 68 |
:content content |
69 | 69 |
:date (local-time:parse-timestring item-date) |
70 | 70 |
:id item-guid |
... | ... |
@@ -81,9 +81,8 @@ |
81 | 81 |
($ (inline author) "> uri" (text) (node)) |
82 | 82 |
($ (inline author) "> email" (text) (node)))))) |
83 | 83 |
|
84 |
-(defmethod %to-feed (xml-dom (type (eql :atom)) &key feed-link) |
|
85 |
- (declare (ignore type) (ignorable feed-link)) |
|
86 |
- ; TODO: store feed-link |
|
84 |
+(defmethod alimenta::-to-feed (xml-dom (type (eql :atom)) &key feed-link) |
|
85 |
+ (declare (ignore type)) |
|
87 | 86 |
(flet ((get-feed-elem (selector) ($ (inline xml-dom) selector (text) (node))) |
88 | 87 |
(get-feed-elem-attr (selector attr) ($ (inline xml-dom) selector (attr attr) (node)))) |
89 | 88 |
(let ((doc-title (get-feed-elem "feed > title")) |
... | ... |
@@ -133,7 +132,7 @@ |
133 | 132 |
(continue c)))) |
134 | 133 |
(defconstants ,@constants))) |
135 | 134 |
|
136 |
-(defmethod %generate-xml ((feed feed) (feed-type (eql :atom)) &key partial) |
|
135 |
+(defmethod generate-xml ((feed feed) (feed-type (eql :atom)) &key partial) |
|
137 | 136 |
(let ((feed-root (or ($1 (inline partial) "feed") |
138 | 137 |
(plump:make-element (plump:make-root) "feed")))) |
139 | 138 |
(prog1 feed-root |
... | ... |
@@ -152,7 +151,7 @@ |
152 | 151 |
))))) |
153 | 152 |
|
154 | 153 |
|
155 |
-(defmethod %generate-xml ((item item) (feed-type (eql :atom)) &key partial) |
|
154 |
+(defmethod generate-xml ((item item) (feed-type (eql :atom)) &key partial) |
|
156 | 155 |
(let ((parent (if (string-equal (tag-name partial) "feed") |
157 | 156 |
partial |
158 | 157 |
(plump:make-element (plump:make-root) "feed")))) |
... | ... |
@@ -244,7 +243,7 @@ |
244 | 243 |
|
245 | 244 |
(deftest to-feed () |
246 | 245 |
(let ((xml (parse +feed1+))) |
247 |
- (symbol-macrolet ((feed (alimenta::%to-feed xml :atom))) |
|
246 |
+ (symbol-macrolet ((feed (alimenta::-to-feed xml :atom))) |
|
248 | 247 |
(should be equal +feed-title+ (slot-value feed 'alimenta:title)) |
249 | 248 |
(should be equal +feed-link-website+ (slot-value feed 'alimenta:link)) |
250 | 249 |
(should be equal +feed-link-self+ (slot-value feed 'alimenta:feed-link)) |
... | ... |
@@ -256,7 +255,7 @@ |
256 | 255 |
|
257 | 256 |
(should be equal +feed-category-term+ |
258 | 257 |
(slot-value |
259 |
- (elt |
|
258 |
+ (elt |
|
260 | 259 |
(slot-value feed 'categories) |
261 | 260 |
0) |
262 | 261 |
'term)) |
... | ... |
@@ -297,7 +296,7 @@ |
297 | 296 |
(deftest generate-xml () |
298 | 297 |
(let* ((xml ($ (inline (parse +entry1+)) "entry" (node))) |
299 | 298 |
(item (alimenta::make-item xml :atom))) |
300 |
- (symbol-macrolet ((generated-xml (alimenta::%generate-xml item :atom))) |
|
299 |
+ (symbol-macrolet ((generated-xml (alimenta::generate-xml item :atom))) |
|
301 | 300 |
(should be equal +title+ |
302 | 301 |
($ (inline generated-xml) "entry > title" (text) (node))) |
303 | 302 |
(should be equal +author+ |
... | ... |
@@ -4,6 +4,9 @@ |
4 | 4 |
(:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora) |
5 | 5 |
(:export #:define-data-class)) |
6 | 6 |
|
7 |
+(defpackage #:alimenta2 |
|
8 |
+ (:use #:cl #:alexandria #:serapeam #:fw.lu #:should-test)) |
|
9 |
+ |
|
7 | 10 |
(defpackage #:alimenta |
8 | 11 |
(:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora) |
9 | 12 |
(:export #:to-feed #:generate-xml #:feed #:title #:link #:items #:feed-link |
... | ... |
@@ -49,6 +49,7 @@ |
49 | 49 |
(slot-value self 'domain)))) |
50 | 50 |
|
51 | 51 |
(defun get-date (str) |
52 |
+ (declare (optimize (debug 3))) |
|
52 | 53 |
(handler-case |
53 | 54 |
(local-time:parse-timestring str) |
54 | 55 |
(local-time::invalid-timestring (c) (declare (ignore c)) |
... | ... |
@@ -59,10 +60,17 @@ |
59 | 60 |
(minute-offset (if (and res (> (length (elt groups 1)) 3)) |
60 | 61 |
(* (signum hour-offset) (parse-integer (elt groups 1) :start 3)) |
61 | 62 |
0))) |
62 |
- (let-each (:be *) |
|
63 |
- (chronicity:parse timestamp) |
|
64 |
- (local-time:timestamp- * minute-offset :minute) |
|
65 |
- (local-time:timestamp- * hour-offset :hour)))))))) |
|
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)))))))))))) |
|
66 | 74 |
|
67 | 75 |
|
68 | 76 |
(defmethod primary-value ((self rss-image)) |
... | ... |
@@ -89,10 +97,10 @@ |
89 | 97 |
(combine (text) (attr "domain")) |
90 | 98 |
(map-apply #'make-category))) |
91 | 99 |
|
92 |
-(defmethod %get-items (xml-dom (feed-type (eql :rss))) |
|
100 |
+(defmethod get-items (xml-dom (feed-type (eql :rss))) |
|
93 | 101 |
($ (inline xml-dom) "channel > item")) |
94 | 102 |
|
95 |
-(defmethod %generate-xml ((item item) (feed-type (eql :rss)) &key partial) |
|
103 |
+(defmethod generate-xml ((item item) (feed-type (eql :rss)) &key partial) |
|
96 | 104 |
(prog1 partial |
97 | 105 |
(let ((item-root (make-element ($1 (inline partial) "channel") "item"))) |
98 | 106 |
(flet ((make-element (tag) (make-element item-root tag))) |
... | ... |
@@ -106,7 +114,7 @@ |
106 | 114 |
"isPermaLink" |
107 | 115 |
"false")))))) |
108 | 116 |
|
109 |
-(defmethod %generate-xml ((feed feed) (feed-type (eql :rss)) &rest r) |
|
117 |
+(defmethod generate-xml ((feed feed) (feed-type (eql :rss)) &rest r) |
|
110 | 118 |
(declare (ignore r)) |
111 | 119 |
(let* ((xml-root (plump:make-root)) |
112 | 120 |
(feed-root (plump:make-element xml-root "rss")) |
... | ... |
@@ -163,7 +171,7 @@ |
163 | 171 |
(local-time:parse-timestring "2016-01-09T23:00:00.000000-0100") |
164 | 172 |
(get-date "Fri, 09 Jan 2016 21:30:00 -0230"))) |
165 | 173 |
|
166 |
-(defmethod %to-feed (xml-dom (type (eql :rss)) &key feed-link) |
|
174 |
+(defmethod alimenta::-to-feed (xml-dom (type (eql :rss)) &key feed-link) |
|
167 | 175 |
; TODO: store feed-link |
168 | 176 |
(flet ((get-channel-element (el) |
169 | 177 |
($ (inline xml-dom) el (text) (node)))) |