Browse code
Added update thread, various reorg/cleanup
fiddlerwoaroof authored on 12/10/2015 05:37:23
Showing 4 changed files
Showing 4 changed files
... | ... |
@@ -12,6 +12,8 @@ |
12 | 12 |
(ql:quickload :ubiquitous) |
13 | 13 |
(ql:quickload :iterate) |
14 | 14 |
(ql:quickload :jonathan) |
15 |
+(ql:quickload :cl-actors) |
|
16 |
+(ql:quickload :simple-tasks) |
|
15 | 17 |
|
16 | 18 |
(declaim (optimize (speed 0) (safety 3) (debug 2))) |
17 | 19 |
|
... | ... |
@@ -19,10 +21,11 @@ |
19 | 21 |
(push (cons "application" "rss+xml") drakma:*text-content-types*) |
20 | 22 |
(push (cons "text" "rss+xml") drakma:*text-content-types*) |
21 | 23 |
|
24 |
+;(load "utils.lisp") |
|
22 | 25 |
(load "rss.lisp") |
23 | 26 |
|
24 | 27 |
(defpackage :whitespace |
25 |
- (:use #:cl #:whitespace.utils #:whitespace.rss #:whitespace.tables)) |
|
28 |
+ (:use #:cl #:whitespace.utils #:whitespace.feeds.rss #:whitespace.tables)) |
|
26 | 29 |
|
27 | 30 |
(in-package plump-dom) |
28 | 31 |
|
... | ... |
@@ -58,6 +61,15 @@ |
58 | 61 |
(defparameter *feeds* (map 'vector (lambda (x) (make-rss-feed x)) docs)))) |
59 | 62 |
|
60 | 63 |
(defparameter *db-connection-info* (ubiquitous:value 'db 'connection 'info)) |
64 |
+(defmacro with-whitespace-db (&body body) |
|
65 |
+ `(postmodern:with-connection *db-connection-info* |
|
66 |
+ ,@body)) |
|
67 |
+(defmacro wc (&body body) |
|
68 |
+ `(with-whitespace-db ,@body)) |
|
69 |
+ |
|
70 |
+(defmacro with-xml-tags (&body body) |
|
71 |
+ `(let ((plump:*tag-dispatchers* plump:*xml-tags*)) |
|
72 |
+ ,@body)) |
|
61 | 73 |
|
62 | 74 |
(defmacro def-markup (name (&rest args) &body body) |
63 | 75 |
`(defmacro ,name ,args |
... | ... |
@@ -302,6 +314,7 @@ |
302 | 314 |
|
303 | 315 |
`((:or (:and .link-header :hover) (.link.closed (:and .link-header :hover))) |
304 | 316 |
:background-color ,(colors:colorscheme-hover-highlight *colorscheme*))))) |
317 |
+ (declare (ignorable main-right-margin)) ; TODO: use this!!! |
|
305 | 318 |
`(200 (:content-type "text/css") ,ss))))) |
306 | 319 |
|
307 | 320 |
(cl-oid-connect:def-route ("/theme/dark.css" (params) :app *app*) |
... | ... |
@@ -348,4 +361,31 @@ |
348 | 361 |
(stop)) |
349 | 362 |
(start)) |
350 | 363 |
|
364 |
+(defun update-feed (url) |
|
365 |
+ (wc |
|
366 |
+ (postmodern:with-transaction () |
|
367 |
+ (upsert-feed (make-rss-feed (with-xml-tags (plump:parse (drakma:http-request url)))))))) |
|
368 |
+ |
|
369 |
+(defun update-all-feeds () |
|
370 |
+ (wc |
|
371 |
+ (let ((urls (postmodern:query (:select 'fetch-url :from 'rss-feed-store)))) |
|
372 |
+ (mapcar (lambda (x) (apply #'update-feed x)) urls)))) |
|
373 |
+ |
|
374 |
+(defun minutes (minutes) (* minutes 60)) |
|
375 |
+ |
|
376 |
+(let (update-thread stop) |
|
377 |
+ (defun start-update-thread () |
|
378 |
+ (setf update-thread |
|
379 |
+ (bordeaux-threads:make-thread |
|
380 |
+ (lambda () |
|
381 |
+ (loop |
|
382 |
+ (update-all-feeds) |
|
383 |
+ (sleep (ubiquitous:value 'update-frequency)) |
|
384 |
+ (when stop |
|
385 |
+ (return-from nil nil)))) |
|
386 |
+ :name "Whitespace Update Thread"))) |
|
387 |
+ (defun stop-update-thread () |
|
388 |
+ (setf stop t) |
|
389 |
+ (setf update-thread nil))) |
|
390 |
+ |
|
351 | 391 |
; vim: foldmethod=marker foldmarker=(,) foldminlines=3 : |
... | ... |
@@ -1,27 +1,60 @@ |
1 | 1 |
(in-package :cl-user) |
2 |
+(use-package :lquery) |
|
2 | 3 |
(declaim (optimize (safety 3) (speed 0) (debug 3))) |
3 | 4 |
|
5 |
+(define-lquery-list-function where-attr (els attr &key is) |
|
6 |
+ (remove-if-not (lambda (y) (string= (plump:attribute y attr) is)) |
|
7 |
+ els)) |
|
8 |
+ |
|
9 |
+(define-lquery-list-function tag-name (nodes &rest tags) |
|
10 |
+ "Manipulate elements on the basis of their tag-name. With no arguments, |
|
11 |
+ return their names else return the corresponding tags." |
|
12 |
+ (if (null tags) |
|
13 |
+ (map 'vector #'plump:tag-name nodes) |
|
14 |
+ (apply #'vector |
|
15 |
+ (loop for node across nodes |
|
16 |
+ if (find (plump:tag-name node) tags :test #'string=) |
|
17 |
+ collect node)))) |
|
18 |
+ |
|
19 |
+ |
|
20 |
+ |
|
4 | 21 |
(load "tables.lisp") |
5 | 22 |
|
6 |
-(defpackage :whitespace.rss |
|
23 |
+(defpackage :whitespace.feeds.autodiscovery |
|
7 | 24 |
(:use #:cl #:alexandria #:postmodern #:lquery #:cl-syntax #:cl-annot.syntax #:cl-annot.class |
8 | 25 |
#:whitespace.tables #:iterate #:whitespace.utils) |
9 | 26 |
(:import-from anaphora it)) |
27 |
+(in-package #:whitespace.feeds.autodiscovery) |
|
28 |
+ |
|
29 |
+(defun discover-feeds (doc) |
|
30 |
+ "Given a plump DOM element, discover any feeds in the document using the link tags. |
|
31 |
+ Returns a vector " |
|
32 |
+ (coerce |
|
33 |
+ (lquery:$ (inline doc) |
|
34 |
+ "link" |
|
35 |
+ (where-attr "rel" :is "alternate") |
|
36 |
+ (combine (attr "href") |
|
37 |
+ (attr "type"))) |
|
38 |
+ 'list)) |
|
39 |
+(export 'discover-feeds) |
|
40 |
+ |
|
41 |
+(defpackage :whitespace.feeds.opml |
|
42 |
+ (:use #:cl #:alexandria #:postmodern #:lquery #:cl-syntax #:cl-annot.syntax #:cl-annot.class |
|
43 |
+ #:whitespace.tables #:iterate #:whitespace.utils) |
|
44 |
+ (:import-from anaphora it)) |
|
45 |
+(in-package #:whitespace.feeds.opml) |
|
10 | 46 |
|
11 | 47 |
|
12 |
-(in-package :whitespace.rss) |
|
13 |
-(cl-annot.syntax:enable-annot-syntax) |
|
14 | 48 |
|
15 |
-(lquery:define-lquery-list-function tag-name (nodes &rest tags) |
|
16 |
- "Manipulate elements on the basis of their tag-name. |
|
17 |
- With no arguments, return their names else return |
|
18 |
- the corresponding tags." |
|
19 |
- (if (null tags) |
|
20 |
- (map 'vector #'plump:tag-name nodes) |
|
21 |
- (apply #'vector |
|
22 |
- (loop for node across nodes |
|
23 |
- if (find (plump:tag-name node) tags :test #'string=) |
|
24 |
- collect node)))) |
|
49 |
+(in-package :cl-user) |
|
50 |
+(defpackage :whitespace.feeds.rss |
|
51 |
+ (:use #:cl #:alexandria #:postmodern #:lquery #:cl-syntax #:cl-annot.syntax #:cl-annot.class |
|
52 |
+ #:whitespace.tables #:iterate #:whitespace.utils #:whitespace.feeds.autodiscovery) |
|
53 |
+ (:import-from anaphora it)) |
|
54 |
+ |
|
55 |
+ |
|
56 |
+(in-package :whitespace.feeds.rss) |
|
57 |
+(cl-annot.syntax:enable-annot-syntax) |
|
25 | 58 |
|
26 | 59 |
@export |
27 | 60 |
(defmacro get-elements (feed &optional (filter nil)) |
... | ... |
@@ -39,12 +72,6 @@ |
39 | 72 |
(defmacro extract-text (selector &optional (default "")) |
40 | 73 |
`(or (lquery:$ ,selector (text) (node)) ,default)) |
41 | 74 |
|
42 |
-(defmacro defserializer ((specializes) &body slots) |
|
43 |
- (with-gensyms (obj o-t p-t) |
|
44 |
- `(defmethod serialize ((,obj ,specializes) &optional (,o-t #'identity) (,p-t #'%default-pair-transform)) |
|
45 |
- (transform-result (,o-t ,p-t) |
|
46 |
- (slots-to-pairs ,obj ,slots))))) |
|
47 |
- |
|
48 | 75 |
@export |
49 | 76 |
(defmacro xml-text-bind (syms &body body) |
50 | 77 |
"Bind the symbols passed in the second arg to the text of the matching |
... | ... |
@@ -84,7 +111,7 @@ |
84 | 111 |
(source :accessor rss-item-source :initarg :source))) |
85 | 112 |
|
86 | 113 |
@export |
87 |
-(defun make-rss-item (item) |
|
114 |
+(defun make-rss-item (item fallback-date) |
|
88 | 115 |
(lquery:initialize item) |
89 | 116 |
(flet ((dehtml (h) (plump:text (plump:parse h))) |
90 | 117 |
(get-category-names (it) ;;; TODO: simplify this---Ask Shinmera on IRC |
... | ... |
@@ -100,16 +127,18 @@ |
100 | 127 |
|
101 | 128 |
(description-raw (normalize-html |
102 | 129 |
(default-when description-element (emptyp description-element) |
103 |
- (extract-text "description")))) |
|
104 |
- |
|
105 |
- (description-munged (dehtml description-raw)) |
|
106 |
- (category (get-category-names (lquery:$ "category")))) |
|
130 |
+ (extract-text "description"))))) |
|
107 | 131 |
;(enclosure) --- TODO: implement comment / enclosure handling |
108 | 132 |
|
109 |
- (xml-text-bind (title link guid pub-date source comments) |
|
133 |
+ (xml-text-bind (title link guid pubdate source comments) |
|
134 |
+ (when (string= pubdate "") |
|
135 |
+ (setf pubdate fallback-date)) |
|
110 | 136 |
(make-instance-from-symbols 'rss-item |
111 |
- item title link description-raw (description description-munged) |
|
112 |
- category guid pub-date source comments))))) |
|
137 |
+ item title link |
|
138 |
+ description-raw |
|
139 |
+ (description (dehtml description-raw)) |
|
140 |
+ (category (get-category-names (lquery:$ "category"))) |
|
141 |
+ guid (pub-date pubdate) source comments))))) |
|
113 | 142 |
;(setf (rss-item-enclosure result) enclosure) -- TODO: comment/enclosure . . . |
114 | 143 |
|
115 | 144 |
|
... | ... |
@@ -120,20 +149,17 @@ |
120 | 149 |
(fetch-url (lquery:$ "channel" (children) (tag-name "atom:link") (filter "[rel=self]") (attr :href) (node))) |
121 | 150 |
(link (lquery:$ "channel > link" (text) (node))) |
122 | 151 |
(link (if (string= link "") (lquery:$ "channel" (children) (tag-name "atom:link") (attr :href) (node)) link)) |
123 |
- (items (lquery:$ "item"))) |
|
152 |
+ (items (lquery:$ "item")) |
|
153 |
+ (last-build (or (lquery:$ "lastBuildDate" (text) (node)) "")) |
|
154 |
+ (pub-date (default-when last-build (string= last-build "") |
|
155 |
+ (lquery:$ "pubDate" (text) (node)))) |
|
156 |
+ (fallback-date (if (string= pub-date "") "2015-01-01 0:0:0+00" pub-date))) |
|
157 |
+ (format t "fallback-date: ~a~%" fallback-date) |
|
124 | 158 |
(xml-text-bind (title description) |
125 | 159 |
(make-instance-from-symbols 'rss-feed |
126 | 160 |
feed title link description channel fetch-url |
127 | 161 |
(items (iterate (for it in-sequence items) |
128 |
- (collecting (make-rss-item it)))))))) |
|
129 |
- |
|
130 |
-@export |
|
131 |
-(defgeneric serialize (cls &optional output-transform pair-transform)) |
|
132 |
- |
|
133 |
- |
|
134 |
-(defmethod serialize ((obj sequence) &optional (o-t #'identity) (p-t #'%default-pair-transform)) |
|
135 |
- (iterate (for item in-sequence obj) |
|
136 |
- (collect (serialize item o-t p-t)))) |
|
162 |
+ (collecting (make-rss-item it fallback-date)))))))) |
|
137 | 163 |
|
138 | 164 |
; These are the interface I'm planning to remove as duplicate |
139 | 165 |
(defserializer (rss-feed) |
... | ... |
@@ -144,13 +170,6 @@ |
144 | 170 |
(defserializer (rss-item) |
145 | 171 |
title link (description description-raw :bind-from description-raw) guid pub-date source) |
146 | 172 |
|
147 |
-; this is the interface to be used |
|
148 |
-(defserializer (rss_feed_store) |
|
149 |
- title link description fetch-url) |
|
150 |
- |
|
151 |
-(defserializer (rss_item_store) |
|
152 |
- title link description fetch-url) |
|
153 |
- |
|
154 | 173 |
(defmethod jonathan:%to-json ((obj rss-feed)) |
155 | 174 |
(jonathan:%to-json (serialize obj #'alexandria:alist-hash-table #'%json-pair-transform))) |
156 | 175 |
|
... | ... |
@@ -164,63 +183,22 @@ |
164 | 183 |
(caar it)))) |
165 | 184 |
,@body))) |
166 | 185 |
|
167 |
-(defgeneric get-dao-for (obj &optional link) |
|
168 |
- ; NOTE: this won't make dao objects for the _items_ when called on the feed! |
|
169 |
- ; also NOTE: this _prefers_ the passed object |
|
170 |
- (:method ((obj rss-feed) &optional linked-objects) |
|
171 |
- (declare (ignore linked-objects)) |
|
172 |
- (with-slots (title link description fetch-url) obj |
|
173 |
- (get-id-for-object (rss_feed_store link) link |
|
174 |
- (make-instance-from-symbols 'rss_feed_store id title link description fetch-url (fetch-defaults t))) )) |
|
175 |
- |
|
176 |
- (:method ((obj rss-item) &optional feed) |
|
177 |
- (with-slots (title link description-raw guid pub-date source) obj |
|
178 |
- (get-id-for-object (rss_item_store guid) guid |
|
179 |
- (let ((result (make-instance-from-symbols 'rss_item_store title link (description description-raw) |
|
180 |
- guid pub-date source feed (fetch-defaults t)))) |
|
181 |
- (unless (null id) |
|
182 |
- (setf (ris-id result) id)) |
|
183 |
- result))))) |
|
184 |
- |
|
185 |
-(define-condition blarg () ((text :initarg text))) |
|
186 |
-@export |
|
187 |
-(defun get-feed-from-dao (rss-feed) |
|
188 |
- (let ((feed-dao (get-dao-for rss-feed))) |
|
189 |
- (list feed-dao |
|
190 |
- ;(error 'blarg :text (format t "~a~%" rss-feed)) |
|
191 |
- (with-slots (items) rss-feed |
|
192 |
- (iterate (for item in items) |
|
193 |
- (collect (get-dao-for item (slot-value feed-dao 'id)))))))) |
|
194 |
- |
|
195 |
- |
|
196 |
-@export |
|
197 |
-(defun upsert-feed (rss-feed) |
|
198 |
- (postmodern:ensure-transaction |
|
199 |
- (destructuring-bind (feed items) (get-feed-from-dao rss-feed) |
|
200 |
- (postmodern:upsert-dao feed) |
|
201 |
- (mapcar #'postmodern:upsert-dao items)))) |
|
202 |
- |
|
203 |
-@export |
|
204 |
-(defun store-item-dao (serialized-rss-item link) |
|
205 |
- (apply #'postmodern:make-dao |
|
206 |
- (list* 'rss_item_store :feed link |
|
207 |
- (iterate (for (k . v) in-sequence serialized-rss-item) |
|
208 |
- (appending (list k v)))))) |
|
209 |
- |
|
210 |
-@export |
|
211 |
-(defun store-feed-dao (serialized-rss-feed &optional link) |
|
212 |
- (declare (ignore link)) |
|
213 |
- (let* ((items nil) |
|
214 |
- (rss_feed (apply #'postmodern:make-dao |
|
215 |
- (cons 'rss_feed_store |
|
216 |
- (iterate (for (k . v) in-sequence serialized-rss-feed) |
|
217 |
- (if (eql k :items) |
|
218 |
- (setf items v) |
|
219 |
- (appending (list k v)))))))) |
|
220 |
- (iterate (for item in items) |
|
221 |
- (store-item-dao (serialize item) |
|
222 |
- (slot-value rss_feed 'id))) |
|
223 |
- rss_feed)) |
|
186 |
+; NOTE: this won't make dao objects for the _items_ when called on the feed! |
|
187 |
+; also NOTE: this _prefers_ the passed object |
|
188 |
+(defmethod get-dao-for ((obj rss-feed) &optional linked-objects) |
|
189 |
+ (declare (ignore linked-objects)) |
|
190 |
+ (with-slots (title link description fetch-url) obj |
|
191 |
+ (get-id-for-object (rss_feed_store link) link |
|
192 |
+ (make-instance-from-symbols 'rss_feed_store id title link description fetch-url (fetch-defaults t))) )) |
|
193 |
+ |
|
194 |
+(defmethod get-dao-for ((obj rss-item) &optional feed) |
|
195 |
+ (with-slots (title link description-raw guid pub-date source) obj |
|
196 |
+ (get-id-for-object (rss_item_store guid) guid |
|
197 |
+ (let ((result (make-instance-from-symbols 'rss_item_store title link (description description-raw) |
|
198 |
+ guid pub-date source feed (fetch-defaults t)))) |
|
199 |
+ (unless (null id) |
|
200 |
+ (setf (ris-id result) id)) |
|
201 |
+ result)))) |
|
224 | 202 |
|
225 | 203 |
(defun get-and-possibly-store-feed (rss-feed) |
226 | 204 |
"Given an rss-feed, return the db's feed-id, persisting it if it doesn't already exist." |
... | ... |
@@ -259,7 +237,10 @@ |
259 | 237 |
@export |
260 | 238 |
(defun deserialize-items (feed-id) |
261 | 239 |
(let ((items (postmodern:query-dao 'rss_item_store |
262 |
- (:select :* :from 'rss_item_store :where (:= :feed feed-id))))) |
|
240 |
+ (:order-by |
|
241 |
+ (:select :* :from 'rss_item_store |
|
242 |
+ :where (:= :feed feed-id)) |
|
243 |
+ (:desc 'pub-date))))) |
|
263 | 244 |
(loop for item in items collect (deserialize-item item)))) |
264 | 245 |
|
265 | 246 |
@export |
... | ... |
@@ -281,15 +262,21 @@ |
281 | 262 |
:where (:= 'reader_user.foreign_id (user-foreign-id user-info)))))) |
282 | 263 |
(apply #'vector (loop for feed in feeds collect (deserialize-feed feed)))))) |
283 | 264 |
|
265 |
+(export |
|
266 |
+ (defun get-feed-from-dao (rss-feed) |
|
267 |
+ (let ((feed-dao (get-dao-for rss-feed))) |
|
268 |
+ (list feed-dao |
|
269 |
+ (with-slots (items) rss-feed |
|
270 |
+ (iterate (for item in items) |
|
271 |
+ (collect (get-dao-for item (slot-value feed-dao 'id))))))))) |
|
272 |
+ |
|
284 | 273 |
|
285 | 274 |
@export |
286 |
-(defun normalize-html (html) |
|
287 |
- (let ((plump-parser:*tag-dispatchers* plump:*html-tags*)) |
|
288 |
- (with-output-to-string (ss) |
|
289 |
- (map 'vector |
|
290 |
- (lambda (x) (plump:serialize (plump:parse (plump:text x)) ss)) |
|
291 |
- html) |
|
292 |
- ss))) |
|
275 |
+(defun upsert-feed (rss-feed) |
|
276 |
+ (postmodern:ensure-transaction |
|
277 |
+ (destructuring-bind (feed items) (get-feed-from-dao rss-feed) |
|
278 |
+ (postmodern:upsert-dao feed) |
|
279 |
+ (mapcar #'postmodern:upsert-dao items)))) |
|
293 | 280 |
|
294 | 281 |
|
295 | 282 |
; \o/ |
... | ... |
@@ -1,6 +1,6 @@ |
1 | 1 |
(in-package :cl-user) |
2 | 2 |
(defpackage :whitespace.tables |
3 |
- (:use #:cl #:alexandria #:postmodern #:annot.class)) |
|
3 |
+ (:use #:cl #:alexandria #:postmodern #:annot.class #:iterate #:whitespace.utils)) |
|
4 | 4 |
(in-package :whitespace.tables) |
5 | 5 |
(cl-annot.syntax:enable-annot-syntax) |
6 | 6 |
|
... | ... |
@@ -74,6 +74,59 @@ |
74 | 74 |
(!foreign "reader_user" "uid" "id" :on-delete :cascade :on-update :cascade) |
75 | 75 |
(!unique '(uid feedid))) |
76 | 76 |
|
77 |
+ |
|
78 |
+@export |
|
79 |
+(defgeneric get-dao-for (obj &optional link) |
|
80 |
+ (:documentation "Take an object an return the equivalent dao object. Use link to specify a single |
|
81 |
+ foreign key relationship---this probably should be generalized further")) |
|
82 |
+ |
|
83 |
+@export |
|
84 |
+(defgeneric serialize (cls &optional output-transform pair-transform)) |
|
85 |
+ |
|
86 |
+@export |
|
87 |
+(defmacro defserializer ((specializes) &body slots) |
|
88 |
+ (with-gensyms (obj o-t p-t) |
|
89 |
+ `(defmethod serialize ((,obj ,specializes) &optional (,o-t #'identity) (,p-t #'%default-pair-transform)) |
|
90 |
+ (transform-result (,o-t ,p-t) |
|
91 |
+ (slots-to-pairs ,obj ,slots))))) |
|
92 |
+ |
|
93 |
+(defmethod serialize ((obj sequence) &optional (o-t #'identity) (p-t #'%default-pair-transform)) |
|
94 |
+ (iterate (for item in-sequence obj) |
|
95 |
+ (collect (serialize item o-t p-t)))) |
|
96 |
+ |
|
97 |
+; this is the interface to be used |
|
98 |
+(defserializer (rss_feed_store) |
|
99 |
+ title link description fetch-url) |
|
100 |
+ |
|
101 |
+(defserializer (rss_item_store) |
|
102 |
+ title link description fetch-url) |
|
103 |
+ |
|
104 |
+@export |
|
105 |
+(defun store-item-dao (serialized-rss-item link) |
|
106 |
+ (pomo:ensure-transaction |
|
107 |
+ (apply #'postmodern:make-dao |
|
108 |
+ (list* 'rss_item_store :feed link |
|
109 |
+ (iterate (for (k . v) in-sequence serialized-rss-item) |
|
110 |
+ (appending (list k v))))))) |
|
111 |
+ |
|
112 |
+@export |
|
113 |
+(defun store-feed-dao (serialized-rss-feed &optional link) |
|
114 |
+ (declare (ignore link)) |
|
115 |
+ (pomo:ensure-transaction |
|
116 |
+ (let* ((items nil) |
|
117 |
+ (rss_feed (apply #'postmodern:make-dao |
|
118 |
+ (cons 'rss_feed_store |
|
119 |
+ (iterate (for (k . v) in-sequence serialized-rss-feed) |
|
120 |
+ (if (eql k :items) |
|
121 |
+ (setf items v) |
|
122 |
+ (appending (list k v)))))))) |
|
123 |
+ (iterate (for item in items) |
|
124 |
+ (handler-case (pomo:with-savepoint store-item |
|
125 |
+ (store-item-dao (serialize item) (slot-value rss_feed 'id))) |
|
126 |
+ (cl-postgres-error:unique-violation ()))) |
|
127 |
+ rss_feed))) |
|
128 |
+ |
|
129 |
+ |
|
77 | 130 |
#| |
78 | 131 |
|
79 | 132 |
(with-connection whitespace::*db-connection-info* |
... | ... |
@@ -66,4 +66,12 @@ |
66 | 66 |
,(make-pairs slots))))) |
67 | 67 |
(export 'slots-to-pairs) |
68 | 68 |
|
69 |
+(defun normalize-html (html) |
|
70 |
+ (let ((plump-parser:*tag-dispatchers* plump:*html-tags*)) |
|
71 |
+ (with-output-to-string (ss) |
|
72 |
+ (prog1 ss |
|
73 |
+ (map 'vector |
|
74 |
+ (lambda (x) (plump:serialize (plump:parse (plump:text x)) ss)) |
|
75 |
+ html))))) |
|
76 |
+(export 'normalize-html) |
|
69 | 77 |
|