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
Showing 9 changed files
- alimenta.asd
- alimenta.lisp
- atom.lisp
- collections-for.lisp
- collections-sbcl-iterators.lisp
- collections.lisp
- fetching.lisp
- package.lisp
- rss.lisp
... | ... |
@@ -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 |