Browse code
feat: add ical parser
Edward authored on 14/04/2021 04:59:35
Showing 1 changed files
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))) |