git.fiddlerwoaroof.com
Browse code

Basic RSS reader implemented

fiddlerwoaroof authored on 01/09/2015 07:26:46
Showing 8 changed files
... ...
@@ -5,15 +5,18 @@
5 5
   :author "Ed L <(format nil \"~a@~a\" \"el-projects\" \"howit.is\")>"
6 6
   :license "2=Clause BSD"
7 7
   :depends-on (:drakma
8
-               :ningle
9
-               :clack
10
-               :cljwt
11
-               :cl-json
12
-               :anaphora
13
-               :alexandria
14
-               :lack-middleware-session
15
-               :sheeple
16
-               :cl-who)
8
+                :ningle
9
+                :clack
10
+                :cljwt
11
+                :cl-json
12
+                :anaphora
13
+                :alexandria
14
+                :lack-middleware-session
15
+                :sheeple
16
+                :lass
17
+                :lquery
18
+                :plump
19
+                :cl-who)
17 20
   :serial t
18 21
   :components ((:file "package")
19 22
                (:file "cl-oid-connect")))
... ...
@@ -33,7 +33,6 @@
33 33
 |#
34 34
 
35 35
 (in-package :cl-oid-connect)
36
-(declaim (optimize (debug 2)))
37 36
 ; Should this be here?
38 37
 (defparameter *oid* (make-instance 'ningle:<app>))
39 38
 (setf drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
... ...
@@ -59,6 +58,10 @@
59 58
 ; goog is well behaved
60 59
 (defparameter *goog-endpoint-schema* (defobject (=endpoint-schema= *goog-info*)))
61 60
 
61
+(defun get-base-url (request) (format nil "~a//~a/oidc_callback"
62
+                                      (lack.request:request-query-parameters)
63
+                                      ))
64
+
62 65
 ; fbook needs personal attention
63 66
 (defproto *fbook-endpoint-schema* (=endpoint-schema= *fbook-info*)
64 67
           ((auth-endpoint "https://www.facebook.com/dialog/oauth")
... ...
@@ -190,14 +193,15 @@
190 193
 
191 194
 
192 195
 (defun oauth2-login-middleware (&key google-info facebook-info)
193
-  (lambda (app)
194
-    (in-package :cl-oid-connect)
195
-    (load-facebook-info facebook-info)
196
-    (load-goog-endpoint-schema)
197
-    (load-google-info google-info)
196
+  (let ((clack-env nil))
197
+    (lambda (app)
198
+      (in-package :cl-oid-connect)
199
+      (load-facebook-info facebook-info)
200
+      (load-goog-endpoint-schema)
201
+      (load-google-info google-info)
198 202
 
199 203
     (def-route ("/login/google" (params) :app app)
200
-      (with-session (cl-oid-connect:session)
204
+      (with-context-variables (session)
201 205
         (let ((state (gen-state 36)))
202 206
           (setf (gethash :state session) state)
203 207
           (with-endpoints *goog-endpoint-schema*
... ...
@@ -224,7 +228,7 @@
224 228
       (let ((received-state (cdr (string-assoc "state" params)))
225 229
             (code (cdr (string-assoc "code" params))))
226 230
         (check-state received-state
227
-                     (with-session (cl-oid-connect:session)
231
+                     (with-context-variables (session)
228 232
                        (with-endpoints *goog-endpoint-schema*
229 233
                          (let* ((a-t (get-access-token *goog-endpoint-schema* code))
230 234
                                 (id-token (assoc-cdr :id--token a-t))
... ...
@@ -239,7 +243,7 @@
239 243
             (code (cdr (string-assoc "code" params))))
240 244
         (with-endpoints *fbook-endpoint-schema*
241 245
           (check-state received-state
242
-                       (with-session (session)
246
+                       (with-context-variables (session)
243 247
                          (let* ((a-t (get-access-token *fbook-endpoint-schema* code))
244 248
                                 (id-token (assoc-cdr :access--token a-t)))
245 249
                            (setf (gethash :userinfo session) (get-user-info *fbook-endpoint-schema* id-token))
... ...
@@ -247,17 +251,17 @@
247 251
                        '(403 '() "Out, vile imposter!")))))
248 252
 
249 253
     (def-route ("/userinfo.json" (params) :app app)
250
-      (with-session (session)
254
+      (with-context-variables (session)
251 255
         (require-login 
252 256
           (with-endpoints  (gethash :endpoint-schema session)
253 257
             (cl-json:encode-json-to-string (gethash :userinfo session))))))
254 258
 
255 259
     (def-route ("/logout" (params) :app app)
256
-      (with-session (session)
260
+      (with-context-variables (session)
257 261
         (setf (gethash :userinfo session) nil)
258 262
         '(302 (:location "/"))))
259 263
 
260
-    app))
264
+    app)))
261 265
 
262 266
 
263 267
 (defmacro redirect-if-necessary (sessionvar &body body)
... ...
@@ -273,41 +277,3 @@
273 277
 
274 278
 (export '(redirect-if-necessary def-route require-login))
275 279
 (export '(oauth2-login-middleware with-session))
276
-
277
-(in-package :cl-user)
278
-
279
-(defparameter *app* (make-instance 'ningle:<app>))
280
-
281
-(cl-oid-connect:def-route ("/login" (params) :app *app*)
282
-  (cl-who:with-html-output-to-string (s)
283
-    (:html
284
-      (:head
285
-        (:title "Login"))
286
-      (:body
287
-        (:div (:a :href "/login/facebook" "Facebook"))
288
-        (:div (:a :href "/login/google" "Google")))))) 
289
-
290
-(defvar *smession* nil)
291
-
292
-(cl-oid-connect:def-route ("/" (params) :app *app*)
293
-  (cl-oid-connect:with-session (*smession*)
294
-    (cl-oid-connect:redirect-if-necessary *smession*
295
-      (cl-oid-connect:require-login 
296
-        (anaphora:sunless (gethash :counter *smession*) (setf anaphora:it 0))
297
-        (incf (gethash :counter *smession*))
298
-        (format nil "~Ath visit<br/>~a<br/><br/>~S<br/>"
299
-                (gethash :counter *smession*)
300
-                (alexandria:hash-table-alist *smession*)
301
-                (alexandria:hash-table-alist (ningle:context :session)))))))
302
-
303
-(setf *handler* (clack:clackup (lack.builder:builder
304
-                                 :backtrace
305
-                                 :session
306
-                                 (funcall
307
-                                   (cl-oid-connect:oauth2-login-middleware
308
-                                     :facebook-info
309
-                                      (truename "/home/edwlan/github_repos/cl-oid-connect/facebook-secrets.json") 
310
-                                     :google-info
311
-                                     (truename "/home/edwlan/github_repos/cl-oid-connect/google-secrets.json"))
312
-                                   *app*)) :port 9090))
313
-
314 280
new file mode 100644
... ...
@@ -0,0 +1,346 @@
1
+(in-package :cl-user)
2
+(ql:quickload :cl-oid-connect)
3
+(ql:quickload :plump)
4
+(ql:quickload :cl-markup)
5
+
6
+(push (cons "application" "rdf+xml") drakma:*text-content-types*)
7
+
8
+(defparameter *app* (make-instance 'ningle:<app>))
9
+
10
+(defclass rss-feed ()
11
+  ((feed :accessor rss-feed-feed
12
+         :initarg :feed)
13
+   (channel :accessor rss-feed-channel)
14
+   (title :accessor rss-feed-title)
15
+   (link :accessor rss-feed-link)
16
+   (description :accessor rss-feed-description)
17
+   (items :accessor rss-feed-items)))
18
+
19
+(defclass rss-item ()
20
+  ((item :accessor rss-item-item  :initarg :item)
21
+   (title :accessor rss-item-title)
22
+   (link :accessor rss-item-link)
23
+   (description-raw :accessor rss-item-description-raw)
24
+   (description :accessor rss-item-description)
25
+   (category :accessor rss-item-category)
26
+   (comments :accessor rss-item-comments)
27
+   (enclosure :accessor rss-item-enclosure)
28
+   (guid :accessor rss-item-guid)
29
+   (pub-date :accessor rss-item-pub-date)
30
+   (source :accessor rss-item-source)))
31
+
32
+(defmacro get-elements (feed &optional (filter nil))
33
+  (let ((feed-sym (gensym))
34
+        (filter-lis `(lambda (x) (and (plump-dom:element-p x) ,@(loop for x in filter
35
+                                                                      collect `(funcall ,x x))))))
36
+    `(let ((,feed-sym ,feed))
37
+       (remove-if-not ,filter-lis (plump:children ,feed-sym)))))
38
+
39
+(defmacro get-elements-by-tagname (feed tagname)
40
+  `(get-elements ,feed ((lambda (x) (string= ,tagname (plump:tag-name x))))))
41
+
42
+(defmacro extract-text (selector &optional (default ""))
43
+  (alexandria:with-gensyms (selector-s)
44
+    `(let ((,selector-s ,selector))
45
+       (if (not (equalp #() (lquery:$ ,selector-s)))
46
+         (lquery:$ ,selector-s (text) (node))
47
+         ,default))))
48
+
49
+(defun make-rss-item (item)
50
+  (lquery:initialize item)
51
+  (flet ((dehtml (h) (plump:text (plump:parse h)))
52
+         (get-category-names (it) ;;; TODO: simplify this---Ask Shinmera on IRC
53
+           (if (not (equalp #() it))
54
+             (map 'vector
55
+                  (lambda (x) (plump:text (elt (plump:children x) 0)))
56
+                  it)
57
+             #())))
58
+    (let* ((result (make-instance 'rss-item :item item))
59
+           (title (extract-text "title"))
60
+           (link (extract-text "link"))
61
+           (description-raw (let ((plump:*html-tags*)
62
+                                  (ss (make-string-output-stream)))
63
+                              (plump:serialize 
64
+                                (plump:parse (extract-text "description"))
65
+                                ss)
66
+                              (get-output-stream-string ss)))
67
+           (description-munged (dehtml (extract-text "description")))
68
+           (category (get-category-names (lquery:$ "category")))
69
+           ;(comments)
70
+           ;(enclosure)
71
+           (guid (extract-text "aaguid"))
72
+           (pub-date (extract-text "pubDate"))
73
+           (source (extract-text "source")))
74
+      (setf (rss-item-title result) title)
75
+      (setf (rss-item-link result) link)
76
+      (setf (rss-item-description-raw result) description-raw)
77
+      (setf (rss-item-description result) description-munged)
78
+      (setf (rss-item-category result) category)
79
+      ;(setf (rss-item-comments result) comment)
80
+      ;(setf (rss-item-enclosure result) enclosur)
81
+      (setf (rss-item-guid result) guid)
82
+      (setf (rss-item-pub-date result) pub-date)
83
+      (setf (rss-item-source result) source)
84
+      result)))
85
+
86
+(defun make-rss-feed (feed)
87
+  (lquery:initialize feed)
88
+  (let* ((result (make-instance 'rss-feed :feed feed))
89
+         (channel (lquery:$ "channel" (text) (node)))
90
+         (title (lquery:$  "title" (text) (node)))
91
+         (link (lquery:$ "link" (text) (node)))
92
+         (description (lquery:$ "description" (text) (node)))
93
+         (items (lquery:$ "item")))
94
+    (setf (rss-feed-channel result) channel)
95
+    (setf (rss-feed-title result) title)
96
+    (setf (rss-feed-link result) link)
97
+    (setf (rss-feed-description result) description)
98
+    (setf (rss-feed-items result)
99
+          (loop for it across items
100
+                collect (make-rss-item it)))
101
+    result))
102
+
103
+
104
+(cl-oid-connect:def-route ("/login" (params) :app *app*)
105
+  (cl-who:with-html-output-to-string (s)
106
+    (:html
107
+      (:head (:title "Login"))
108
+      (:body
109
+        (:div
110
+          :class "login-button facebook"
111
+          (:a :href "/login/facebook" "Facebook"))
112
+        (:div
113
+          :class "login-button google"
114
+          (:a :href "/login/google" "Google")))))) 
115
+
116
+(defparameter *feed-urls*
117
+  #(
118
+    "http://www.reddit.com/r/lisp.rss"
119
+    "http://www.reddit.com/r/scheme.rss"
120
+    "http://www.reddit.com/r/prolog.rss"
121
+    "http://www.reddit.com/r/haskell.rss"
122
+    "http://www.reddit.com/r/roguelikedev.rss"
123
+    "http://www.reddit.com/r/roguelikes.rss"
124
+    "http://www.reddit.com/r/talesfromtechsupport.rss"
125
+    ))
126
+
127
+(let
128
+  ((plump-parser:*tag-dispatchers* plump-parser:*xml-tags*))
129
+  (defparameter *docs* (map 'vector
130
+                            (lambda (x) 
131
+                              (format t "~a~%" x)
132
+                              (unwind-protect (plump-parser:parse
133
+                                                (drakma:http-request x))))
134
+                            *feed-urls*)))
135
+
136
+(defparameter *feeds* (map 'vector (lambda (x) (unwind-protect (make-rss-feed x))) *docs*))
137
+
138
+(defclass colorscheme ()
139
+  ((background :accessor colorscheme-background :initform "#002b36")
140
+   (foreground :accessor colorscheme-foreground :initform "#839496")
141
+   (accent     :accessor colorscheme-accent     :initform "#586e75" )
142
+   (base03     :accessor colorscheme-base03      :initform "#002b36")
143
+   (base02     :accessor colorscheme-base02      :initform "#073642")
144
+   (base01     :accessor colorscheme-base01      :initform "#586e75")
145
+   (base00     :accessor colorscheme-base00      :initform "#657b83")
146
+   (base0      :accessor colorscheme-base0       :initform "#839496")
147
+   (base1      :accessor colorscheme-base1       :initform "#93a1a1")
148
+   (base2      :accessor colorscheme-base2       :initform "#eee8d5")
149
+   (base3      :accessor colorscheme-base3       :initform "#fdf6e3")
150
+   (yellow     :accessor colorscheme-yellow      :initform "#b58900")
151
+   (orange     :accessor colorscheme-orange      :initform "#cb4b16")
152
+   (red        :accessor colorscheme-red         :initform "#dc322f")
153
+   (magenta    :accessor colorscheme-magenta     :initform "#d33682")
154
+   (violet     :accessor colorscheme-violet      :initform "#6c71c4")
155
+   (blue       :accessor colorscheme-blue        :initform "#268bd2")
156
+   (cyan       :accessor colorscheme-cyan        :initform "#2aa198")
157
+   (green      :accessor colorscheme-green       :initform "#859900")))
158
+
159
+(defgeneric accentize (colorscheme accent)) 
160
+(defmethod accentize ((colorscheme colorscheme) accent)
161
+  (setf (colorscheme-accent colorscheme) (funcall accent colorscheme)))
162
+
163
+(defgeneric rebase (colorscheme))
164
+(defmethod rebase ((colorscheme colorscheme))
165
+  (macrolet
166
+    ((swap-color (obj slot color1 color2)
167
+       `(setf (,slot ,obj)
168
+             (if (string= (,slot ,obj) (,color1 ,obj))
169
+               (,color2 ,obj)
170
+               (,color1 ,obj)))))
171
+    ; Note that swap-color doesn't use gensyms: so don't run functions in invocation
172
+    (swap-color colorscheme colorscheme-foreground colorscheme-base0 colorscheme-base0)
173
+    (swap-color colorscheme colorscheme-accent colorscheme-base1 colorscheme-base01)
174
+    (swap-color colorscheme colorscheme-background colorscheme-base3 colorscheme-base03)
175
+    colorscheme))
176
+
177
+
178
+(defparameter *colorscheme* (make-instance 'colorscheme))
179
+(rebase *colorscheme*)
180
+(accentize *colorscheme* #'colorscheme-blue)
181
+
182
+;rebase  $base3, $base2, $base1, $base0,$base00,$base01,$base02,$base03
183
+;rebase $base03,$base02,$base01,$base00 ,$base0 ,$base1 ,$base2 ,$base3
184
+
185
+
186
+(cl-oid-connect:def-route ("/" (params) :app *app*)
187
+  (flet ((combine-unit-q (quant unit) (format nil "~d~a" quant unit)))
188
+    (let* ((header-height 10)
189
+           (height-units "vh")
190
+           (ss (lass:compile-and-write
191
+                 `(* :color ,(colorscheme-foreground *colorscheme*))
192
+
193
+                 `(body :background-color ,(colorscheme-background *colorscheme*))
194
+
195
+                 `((:or h1 h2 h3 h4 h5 h6) :color ,(colorscheme-accent *colorscheme*))
196
+
197
+                 `(header
198
+                    :border-bottom "thin" "solid" ,(colorscheme-foreground *colorscheme*)
199
+                    :height ,(combine-unit-q header-height height-units)
200
+                    :font-size ,(combine-unit-q (* 0.75 header-height) height-units)
201
+                    :line-height ,(combine-unit-q header-height height-units))
202
+
203
+                 `((:or a (:and a :visited) (:and a :active) code.url)
204
+                   :color ,(colorscheme-accent *colorscheme*))
205
+
206
+                 `(section#sidebar
207
+                    ((ul.menu li a)
208
+                     ((+ a)
209
+                      :border-top "thin" "solid" ,(colorscheme-accent *colorscheme*))
210
+                     ((:and li :hover)
211
+                      :background-color ,(colorscheme-foreground *colorscheme*)
212
+                      :color ,(colorscheme-background *colorscheme*))))
213
+
214
+                 `(.feed :border thin solid ,(colorscheme-foreground *colorscheme*))
215
+                 `(.link
216
+                    :border-top thin solid ,(colorscheme-foreground *colorscheme*)
217
+                    :border-bottom thin solid ,(colorscheme-foreground *colorscheme*)
218
+
219
+                    (.link-info
220
+                      :background ,(colorscheme-foreground *colorscheme*)
221
+                      :color ,(colorscheme-background *colorscheme*)
222
+                      :border "thin" "solid" ,(colorscheme-foreground *colorscheme*)
223
+
224
+                      (.link-url 
225
+                        ;:color ,(colorscheme-cyan *colorscheme*)
226
+                        :color ,(colorscheme-background *colorscheme*))
227
+                      (.link-date
228
+                        :color ,(colorscheme-background *colorscheme*)))))))
229
+
230
+      `(200 (:content-type "text/css") ,ss))))
231
+
232
+(defmacro item-markup (item)
233
+  (alexandria:with-gensyms (item-s)
234
+    `(let ((,item-s ,item))
235
+       (cl-markup:markup
236
+         (:li :class "link closed"
237
+          (:section :class "link-header"
238
+           (:h4 (rss-item-title ,item-s))
239
+           (:p :class "link-info"
240
+            (:a :target "_blank" :href (rss-item-link ,item-s)
241
+             (:span :class "link-url" (rss-item-link ,item-s)))
242
+            (:span :class "link-date") (rss-item-pub-date ,item-s)))
243
+          (:section :class "link-content"
244
+           (cl-markup:raw (rss-item-description-raw ,item-s))))))))
245
+
246
+(defmacro feed-markup (feed-v fc-v)
247
+  (alexandria:with-gensyms (feed fc)
248
+    `(let ((,feed ,feed-v)
249
+           (,fc ,fc-v))
250
+       (cl-markup:markup
251
+         (:section :class "feed" :id (format nil "feed-~a" ,fc)
252
+          (:section :class "feed-header"
253
+           (:h2 (rss-feed-title ,feed))
254
+           (:h3 (rss-feed-description ,feed))) 
255
+          (:ul :class "post-list"
256
+           (loop for item in (rss-feed-items ,feed)
257
+                 collect (item-markup item))))))))
258
+
259
+(defmacro feedlist-markup (feedlist-v)
260
+  (alexandria:with-gensyms (feedlist)
261
+    `(let ((,feedlist ,feedlist-v))
262
+       (cl-markup:markup*
263
+         `(:ul :class "menu"
264
+           ,@(loop for feed across ,feedlist
265
+                   count feed into feed-count
266
+                   collect
267
+                   (list :li
268
+                         (list :a
269
+                               :href (format nil "#feed-~a" feed-count)
270
+                               (rss-feed-title feed)))))))))
271
+
272
+(defmacro base-template ()
273
+  `(cl-markup:html5
274
+     (:head
275
+       (:title "My Feeds")
276
+       (:script :src "https://code.jquery.com/jquery-2.1.4.min.js" :type "text/javascript" "")
277
+       (:script :src "/static/js/fold.js" :type "text/javascript" "")
278
+       (:link :rel "stylesheet" :href "/static/css/main.css")
279
+       (:link :rel "stylesheet" :href "/theme.css"))
280
+     (:body
281
+       (:header
282
+         (:h1 "Worricow"))
283
+       (:section :id "content"
284
+        (:section :id "sidebar"
285
+         (cl-markup:raw (feedlist-markup *feeds*)))
286
+        (:main
287
+          (loop for feed across *feeds*
288
+                count feed into feed-count
289
+                collect
290
+                (feed-markup feed feed-count))))
291
+       (:footer))))
292
+
293
+(defun base-template-f () (base-template))
294
+
295
+;(cl-oid-connect:def-route ("/" (params) :app *app*)
296
+;  (ningle:with-context-variables (session)
297
+;    (cl-oid-connect:redirect-if-necessary session
298
+;      (cl-oid-connect:require-login 
299
+;        (anaphora:sunless (gethash :counter session) (setf anaphora:it 0))
300
+;        (incf (gethash :counter session))
301
+;        (format nil "~Ath visit<br/>~a<br/><br/>"
302
+;                (gethash :counter session))))))
303
+
304
+
305
+(cl-oid-connect:def-route ("/feeds/:feeds/html" (params) :app *app*)
306
+  (ningle.context:with-context-variables (session)
307
+    (cl-oid-connect:require-login
308
+      (let* ((feedlist-s (cdr (assoc :feeds params)))
309
+             (feedlist (mapcar #'parse-integer (split-sequence:split-sequence #\SPACE feedlist-s)))
310
+             (*feeds* (gethash :feeds session *feeds*))
311
+             (*feeds* (make-array (list (length feedlist))
312
+                                  :initial-contents (loop for x in feedlist
313
+                                                          collect (elt *feeds* x)))))
314
+        (base-template-f)))))
315
+
316
+(cl-oid-connect:def-route ("/" (params) :app *app*)
317
+  (ningle.context:with-context-variables (session)
318
+    (cl-oid-connect:require-login
319
+      (cl-oid-connect:require-login 
320
+        (let ((*feeds* (gethash :feeds session *feeds*)))
321
+          (base-template-f))))))
322
+
323
+(defvar *handler* nil)
324
+
325
+
326
+(defun start (tmp)
327
+  (let ((server (if (> (length tmp) 1)
328
+                  (intern (string-upcase (elt tmp 1)) 'keyword)
329
+                  :hunchentoot)))
330
+   (push (clack:clackup
331
+          (lack.builder:builder
332
+            :backtrace
333
+            :session
334
+            :csrf
335
+            (:static :path "/static/" :root #p"./static/")
336
+            (funcall
337
+              (cl-oid-connect:oauth2-login-middleware
338
+                :facebook-info (truename "~/github_repos/cl-oid-connect/facebook-secrets.json") 
339
+                :google-info (truename "~/github_repos/cl-oid-connect/google-secrets.json"))
340
+              *app*)) :port 9090 :server server)
341
+        *handler*))
342
+  (loop (mp:process-wait))
343
+  )
344
+
345
+
346
+
0 347
new file mode 100644
... ...
@@ -0,0 +1,106 @@
1
+(:import "url(https://fonts.googleapis.com/css?family=Lato:400,100,300,400italic,300italic,700,700italic,900&subset=latin,latin-ext)")
2
+(:import "url(https://fonts.googleapis.com/css?family=Caudex)")
3
+
4
+(*
5
+  :box-sizing "border-box"
6
+  :margin "0px"
7
+  :padding "0px"
8
+  :font-family "Lato")
9
+
10
+((:or ul ol)
11
+ :list-style "none")
12
+
13
+((:or h1 h2)
14
+ :font-size "153.9%")
15
+(:h3
16
+  :font-size "146.5%")
17
+((:or h4 h5 h6)
18
+ :font-size "138.5%")
19
+
20
+(((:or ul) + (:or h1 h2 h3 h4 h5 h6))
21
+ :width "initial")
22
+
23
+(header
24
+  :color "white"
25
+  (h1
26
+    :font-family "Caudex"
27
+    :font-size inherit
28
+    :margin-top "0em"
29
+    :padding-left "1em"
30
+    :font-weight 200))
31
+
32
+(section#sidebar
33
+  :width "20vw"
34
+  :height "90vh"
35
+  :position "fixed"
36
+  :overflow "auto"
37
+  (ul.menu
38
+    :text-align right
39
+    :font-variant small-cap
40
+    (li
41
+      (a
42
+        :width "100%"
43
+        :display block
44
+        :padding "1em"
45
+        :color inherit
46
+        :text-decoration none
47
+        :font-weight 700
48
+        :font-size "125%"
49
+        ))))
50
+
51
+(main
52
+  :padding-right "20vw"
53
+  :width "80vw"
54
+  :height "90vh"
55
+  :float "right"
56
+  :clear "right"
57
+  :overflow "auto")
58
+
59
+(.feed-header
60
+  :padding "1em"
61
+  :padding-bottom "0em")
62
+
63
+((:and .link :before)
64
+ :content "-")
65
+
66
+((:and .link.closed :before)
67
+ :content "+")
68
+
69
+((.link.closed .link-content) 
70
+  :max-height "0px"
71
+  :padding "0em")  
72
+
73
+(.link
74
+  :text-decoration none
75
+  :display block
76
+  :padding "1em"
77
+  :padding-bottom "0em"
78
+  :overflow hidden
79
+  :font-size "0.8em"
80
+  (.link-header
81
+    :cursor pointer
82
+    (h4
83
+      :margin-bottom "0.5em"
84
+      :display "inline-block")) 
85
+  (.link-info
86
+    :margin-left "-1em"
87
+    :margin-right "-1em"
88
+    :padding-left "1em"
89
+    :padding-right "1em"
90
+    (.link-url 
91
+      :float "left"
92
+      )
93
+    (.link-date
94
+      :float "right"
95
+      :display block))
96
+  ((:and .link-info :after)
97
+   :content "\" \""
98
+   :display block
99
+   :clear both)
100
+  (.link-content
101
+    :transition "max-height 0.2s ease"))
102
+
103
+(.feed
104
+  :overflow "hidden"
105
+  )
106
+
... ...
@@ -12,6 +12,9 @@
12 12
     #:drakma
13 13
     ;#:lack-middleware-session
14 14
     #:ningle
15
+    #:lass
16
+    #:lquery
17
+    #:plump
15 18
     #:sheeple)
16 19
   (:export
17 20
     #:redirect-if-necessary
18 21
new file mode 100644
... ...
@@ -0,0 +1,17 @@
1
+(chain ($ document)
2
+       (ready
3
+         (lambda ()
4
+           (chain ($ ".link-header")
5
+                  (click
6
+                    (lambda ()
7
+                      (chain ($ this)
8
+                             (siblings ".link-content")
9
+                             (each (lambda ()
10
+                                     (if (= (chain ($ this) (css "max-height")) "0px")
11
+                                       (chain ($ this)
12
+                                              (css "max-height" (@ this scroll-height)))
13
+                                       (chain ($ this)
14
+                                              (css "max-height" "0px"))))))
15
+                      (chain ($ this)
16
+                             (parent)
17
+                             (toggle-class "closed"))))))))
0 18
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+@import url(https://fonts.googleapis.com/css?family=Lato:400,100,300,400italic,300italic,700,700italic,900&subset=latin,latin-ext);@import url(https://fonts.googleapis.com/css?family=Caudex);*{box-sizing:border-box;margin:0px;padding:0px;font-family:Lato;}ul,ol{list-style:none;}h1,h2{font-size:153.9%;}:h3{font-size:146.5%;}h4,h5,h6{font-size:138.5%;}ul + h1,ul + h2,ul + h3,ul + h4,ul + h5,ul + h6{width:initial;}header{color:white;}header h1{font-family:Caudex;font-size:inherit;margin-top:0em;padding-left:1em;font-weight:200;}section#sidebar{width:20vw;height:90vh;position:fixed;overflow:auto;}section#sidebar ul.menu{text-align:right;font-variant:small-cap;}section#sidebar ul.menu li a{width:100%;display:block;padding:1em;color:inherit;text-decoration:none;font-weight:700;font-size:125%;}main{padding-right:20vw;width:80vw;height:90vh;float:right;clear:right;overflow:auto;}.feed-header{padding:1em;padding-bottom:0em;}.link:before{content:-;}.link.closed:before{content:+;}.link.closed .link-content{max-height:0px;padding:0em;}.link{text-decoration:none;display:block;padding:1em;padding-bottom:0em;overflow:hidden;font-size:0.8em;}.link .link-header{cursor:pointer;}.link .link-header h4{margin-bottom:0.5em;display:inline-block;}.link .link-info{margin-left:-1em;margin-right:-1em;padding-left:1em;padding-right:1em;}.link .link-info .link-url{float:left;}.link .link-info .link-date{float:right;display:block;}.link .link-info:after{content:" ";display:block;clear:both;}.link .link-content{-moz-transition:max-height 0.2s ease;-o-transition:max-height 0.2s ease;-webkit-transition:max-height 0.2s ease;-ms-transition:max-height 0.2s ease;transition:max-height 0.2s ease;}.feed{overflow:hidden;}
0 2
\ No newline at end of file
1 3
new file mode 100644
... ...
@@ -0,0 +1,9 @@
1
+$(document).ready(function () {
2
+    return $('.link-header').click(function () {
3
+        $(this).siblings('.link-content').each(function () {
4
+            return $(this).css('max-height') === '0px' ? $(this).css('max-height', this.scrollHeight) : $(this).css('max-height', '0px');
5
+        });
6
+        return $(this).parent().toggleClass('closed');
7
+    });
8
+});
9
+