git.fiddlerwoaroof.com
Browse code

feat: add ical parser

Edward authored on 14/04/2021 04:59:35
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,312 @@
1
+(defpackage :fwoar.lisp-sandbox.ical-parser
2
+  (:use :cl )
3
+  (:export ))
4
+(in-package :fwoar.lisp-sandbox.ical-parser)
5
+
6
+(defvar *data* (alexandria:read-file-into-string "~/test.ics"))
7
+
8
+(defun next-section (s &optional start)
9
+  (if start
10
+      (search "BEGIN" s :start2 start)
11
+      (search "BEGIN" s)))
12
+
13
+(defmacro str->stream ((op arg &rest r))
14
+  (alexandria:once-only (arg)
15
+    (alexandria:with-gensyms (s)
16
+      `(with-input-from-string (,s ,arg)
17
+         (,op ,s ,@r)))))
18
+
19
+(defun get-line (stream)
20
+  (loop for line = (read-line stream nil)
21
+        while line
22
+        collect line into results
23
+        while (eql #\space (peek-char nil stream nil))
24
+        finally (return (when results
25
+                          (string-right-trim
26
+                           '(#\newline #\return)
27
+                           (serapeum:string-replace-all
28
+                            #1=#.(coerce (list #\return #\space)
29
+                                         'string)
30
+                            (serapeum:string-join results "")
31
+                            ""))))))
32
+
33
+(defgeneric handle-begin (client block)
34
+  (:method (_ block)
35
+    (format t "~&;; >>>~a~%" block)))
36
+(defgeneric handle-end (client block)
37
+  (:method (_ block)
38
+    (format t "~&;; <<<~a~%" block)))
39
+(defgeneric handle-line (client tag params content)
40
+  (:method (_ tag params content)
41
+    (format t "~&;; - ~s•~s•~s~%" tag params content)))
42
+(defun process-ics (client file)
43
+  (let ((states '())
44
+        (new-keywords '()))
45
+    (unwind-protect
46
+         (labels ((normalize (inp)
47
+                    (multiple-value-bind (kw existing?)
48
+                        (alexandria:make-keyword (string-upcase inp))
49
+                      (unless existing?
50
+                        (push kw new-keywords))
51
+                      kw))
52
+                  (%handle-block-delimiter (tag type)
53
+                    (push type states)
54
+                    (ecase tag
55
+                      ((:begin) (handle-begin client type))
56
+                      ((:end) (handle-end client type))))
57
+                  (parse-params (inp)
58
+                    (destructuring-bind (head params) (fwoar.string-utils:partition #\; inp)
59
+                      (values head
60
+                              (when params
61
+                                (map 'list
62
+                                     (data-lens:• (data-lens:transform-head #'normalize)
63
+                                                  (serapeum:op
64
+                                                    (fwoar.string-utils:partition #\= _)))
65
+                                     (fwoar.string-utils:split #\; params))))))
66
+                  (parse-property (it)
67
+                    (destructuring-bind (s e) (fwoar.string-utils:partition #\: it)
68
+                      (multiple-value-bind (head params) (parse-params s)
69
+                        (list (normalize head)
70
+                              params
71
+                              e))))
72
+                  (%handle-line (it)
73
+                    (apply 'handle-line client it)))
74
+           (with-input-from-string (s file)
75
+             (loop for line = (get-line s)
76
+                   for (%tag tagged) = (if line
77
+                                           (fwoar.string-utils:partition #\: line)
78
+                                           '(nil nil))
79
+                   for tag = (fw.lu:may (normalize %tag))
80
+                   while line
81
+                   do (case tag
82
+                        ((:begin)
83
+                         (%handle-block-delimiter tag (normalize tagged)))
84
+                        ((:end)
85
+                         (%handle-block-delimiter tag (normalize tagged)))
86
+                        (t (%handle-line (parse-property line)))))))
87
+      (mapc 'unintern new-keywords))))
88
+
89
+(fw.lu:defclass+ emit-sql ()
90
+  ((%lines :accessor lines :initform ())
91
+   (%tzid :accessor tzid :initform nil)
92
+   (%db :reader db :initarg :db)))
93
+(defmethod handle-begin ((client emit-sql) block)
94
+  (values))
95
+(defmethod handle-end :after ((client emit-sql) block)
96
+  (setf (lines client) nil))
97
+(defmethod handle-end :before ((client emit-sql) block)
98
+  (values))
99
+(defmethod handle-end ((client emit-sql) (block (eql :vevent)))
100
+  (macrolet ((get-setter ()
101
+               `(sxql:set= ,@(mapcan (lambda (it)
102
+                                       (list it `(serapeum:assocadr
103
+                                                  ,(alexandria:make-keyword
104
+                                                    (substitute #\- #\_
105
+                                                                (string it)))
106
+                                                  (lines client))))
107
+                                     '(:X_APPLE_TRAVEL_ADVISORY_BEHAVIOR
108
+                                       :VEVENT :VALARM :RECURRENCE_ID
109
+                                       :ORGANIZER :LAST_MODIFIED
110
+                                       :EXDATE :CREATED :ATTENDEE
111
+                                       :ATTENDEE :ATTACH :CATEGORIES
112
+                                       :DESCRIPTION :DTEND :DTSTAMP
113
+                                       :DTSTART :GEO :LOCATION :RRULE
114
+                                       :SEQUENCE :STATUS :SUMMARY
115
+                                       :TRANSP :UID :URL
116
+                                       :X_ALT_DESC)))))
117
+    (multiple-value-bind (query params)
118
+        (sxql:yield
119
+         (sxql:insert-into :vevent
120
+           (get-setter)))
121
+      (apply 'sqlite:execute-single
122
+             (db client)
123
+             query
124
+             params))))
125
+(defmethod handle-line ((client emit-sql) tag params content)
126
+  (push (list tag content)
127
+        (lines client)))
128
+
129
+(defparameter +datetime-scanner+
130
+  (cl-ppcre:create-scanner
131
+   '(:SEQUENCE
132
+     :START-ANCHOR
133
+     ;; date yyyy-mm-dd
134
+     (:REGISTER (:GREEDY-REPETITION 4 4 :DIGIT-CLASS))
135
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
136
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
137
+     #\T
138
+     ;; time hh-mm-ss
139
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
140
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
141
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
142
+     ;; tz
143
+     (:GREEDY-REPETITION 0 1 (:REGISTER #\Z))
144
+     :END-ANCHOR)))
145
+(defparameter +date-scanner+
146
+  (cl-ppcre:create-scanner
147
+   '(:SEQUENCE
148
+     :START-ANCHOR
149
+     ;; date yyyy-mm-dd
150
+     (:REGISTER (:GREEDY-REPETITION 4 4 :DIGIT-CLASS))
151
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
152
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
153
+     :END-ANCHOR)))
154
+(defparameter +time-scanner+
155
+  (cl-ppcre:create-scanner
156
+   '(:SEQUENCE
157
+     :START-ANCHOR
158
+     ;; time hh-mm-ss
159
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
160
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
161
+     (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
162
+     ;; tz
163
+     (:GREEDY-REPETITION 0 1 (:REGISTER #\Z))
164
+     :END-ANCHOR)))
165
+
166
+(defparameter +sqlite-format+
167
+  '((:YEAR 4) #\- (:MONTH 2) #\- (:DAY 2)
168
+    #\space (:HOUR 2) #\: (:MIN 2) #\: (:SEC 2)
169
+    :GMT-OFFSET-OR-Z))
170
+
171
+(defun parse-datetime (time timezone)
172
+  (trivia:ematch (nth-value 1 (cl-ppcre:scan-to-strings +datetime-scanner+ time))
173
+    (#(ye mo da ho mi se tz)
174
+      (local-time:encode-timestamp
175
+       0
176
+       (fw.lu:if-let* ((se (parse-integer se))
177
+                       (_ (= 60 se)))
178
+         59
179
+         se)
180
+       (parse-integer mi) (parse-integer ho)
181
+       (parse-integer da) (parse-integer mo) (parse-integer ye)
182
+       :timezone (if (equal tz "Z")
183
+                     local-time:+utc-zone+
184
+                     timezone)))))
185
+
186
+(defun parse-date (time)
187
+  (trivia:ematch (nth-value 1 (cl-ppcre:scan-to-strings +date-scanner+ time))
188
+    (#(ye mo da)
189
+      (local-time:encode-timestamp
190
+       0 0 0 0
191
+       (parse-integer da) (parse-integer mo) (parse-integer ye)))))
192
+
193
+;;what do I do for the date here???
194
+#+(or)
195
+(defun parse-time (time)
196
+  (trivia::match (nth-value 1 (cl-ppcre:scan-to-strings +time-scanner+ time))
197
+    (#(ho mi se)
198
+      (local-time:make-timestamp
199
+       0 0 0 0
200
+       (parse-integer da) (parse-integer mo) (parse-integer ye)))))
201
+
202
+(defun handle-ical-date (client tag params content)
203
+  (push (list tag
204
+              (local-time:format-timestring
205
+               nil
206
+               (case (alexandria:make-keyword
207
+                      (string-upcase
208
+                       (serapeum:assocadr :value params)))
209
+                 (:date (parse-date content))
210
+                 (t (parse-datetime content
211
+                                    (or (local-time:find-timezone-by-location-name
212
+                                         (serapeum:assocadr :tzid params))
213
+                                        (tzid client)))))
214
+               :format +sqlite-format+))
215
+        (lines client)))
216
+
217
+(defmethod handle-line ((client emit-sql) (tag (eql :dtstart)) params content)
218
+  (handle-ical-date client tag params content))
219
+(defmethod handle-line ((client emit-sql) (tag (eql :dtend)) params content)
220
+  (handle-ical-date client tag params content))
221
+(defmethod handle-line ((client emit-sql) (tag (eql :created)) params content)
222
+  (handle-ical-date client tag params content))
223
+(defmethod handle-line ((client emit-sql) (tag (eql :dtstamp)) params content)
224
+  (handle-ical-date client tag params content))
225
+(defmethod handle-line :after ((client emit-sql) (tag (eql :dtstart)) params content)
226
+  (format t "~&~s ~s~%~4t~s~%~4t~s~%" tag params
227
+          content
228
+          (parse-time content
229
+                      (or (local-time:find-timezone-by-location-name
230
+                           (serapeum:assocadr :tzid params))
231
+                          (tzid client)))))
232
+
233
+(defun setup-sql ()
234
+  (sxql:yield
235
+   (sxql:create-table (:vevent :if-not-exists t)
236
+       ((:ATTACH :type 'text)
237
+        (:ATTENDEE :type 'text)
238
+        (:CATEGORIES :type 'text)
239
+        (:CREATED :type 'text)
240
+        (:DESCRIPTION :type 'text)
241
+        (:DTEND :type 'text)
242
+        (:DTSTAMP :type 'text)
243
+        (:DTSTART :type 'text)
244
+        (:EXDATE :type 'text)
245
+        (:GEO :type 'text)
246
+        (:LAST_MODIFIED :type 'text)
247
+        (:LOCATION :type 'text)
248
+        (:ORGANIZER :type 'text)
249
+        (:RECURRENCE_ID :type 'text)
250
+        (:RRULE :type 'text)
251
+        (:SEQUENCE :type 'text)
252
+        (:STATUS :type 'text)
253
+        (:SUMMARY :type 'text)
254
+        (:TRANSP :type 'text)
255
+        (:UID :type 'text)
256
+        (:URL :type 'text)
257
+        (:VALARM :type 'text)
258
+        (:VEVENT :type 'text)
259
+        (:X_ALT_DESC :type 'text)
260
+        (:X_APPLE_TRAVEL_ADVISORY_BEHAVIOR :type 'text))
261
+     (sxql:primary-key '(:sequence :uid :recurrence_id)))))
262
+
263
+(defun ics->sqlite (fn data)
264
+  (sqlite:with-open-database (db fn)
265
+    (sqlite:execute-non-query
266
+     db (sxql:yield (sxql:drop-table :vevent :if-exists t)))
267
+    (sqlite:execute-non-query
268
+     db (setup-sql))
269
+    (sqlite:with-transaction db
270
+      (process-ics (emit-sql db) data))))
271
+
272
+
273
+(fw.lu:defclass+ build-tree ()
274
+  ((%history :accessor history :initform ())))
275
+
276
+(defmethod handle-begin ((client build-tree) block)
277
+  (push (list block) (history client)))
278
+(defmethod handle-end ((client build-tree) block)
279
+  (progn (when (cdr (history client))
280
+           (let ((last (pop (history client))))
281
+             (push (nreverse last)
282
+                   (car (history client)))))))
283
+(defmethod handle-line ((client build-tree) tag params content)
284
+  (push (list tag params content)
285
+        (car (history client))))
286
+
287
+(defun ics->tree (data)
288
+  (let ((client (build-tree)))
289
+    (process-ics client data)
290
+    (nreverse (car (history client)))))
291
+
292
+(fw.lu:defclass+ one-vevent ()
293
+  ((%started :accessor started :initform nil)
294
+   (%lines :accessor lines :initform nil)))
295
+
296
+(defmethod handle-begin ((client one-vevent) block)
297
+  (when (eql block :vevent)
298
+    (push block (started client))))
299
+
300
+(defmethod handle-line ((client one-vevent) tag params content)
301
+  (when (started client)
302
+    (push (list tag params content)
303
+          (lines client))))
304
+
305
+(defmethod handle-end ((client one-vevent) block)
306
+  (when (eql block :vevent)
307
+    (throw 'vevent
308
+      (nreverse (lines client)))))
309
+
310
+(defun extract-one-vevent (data)
311
+  (catch 'vevent
312
+    (process-ics (one-vevent) data)))