aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--module/datetime.scm56
-rw-r--r--module/datetime/util.scm5
-rw-r--r--module/entry-points/benchmark.scm8
-rw-r--r--module/entry-points/html.scm5
-rw-r--r--module/output/html.scm17
-rw-r--r--module/util/exceptions.scm21
-rw-r--r--module/vcomponent/base.scm30
-rw-r--r--module/vcomponent/datetime.scm8
-rw-r--r--module/vcomponent/parse.scm333
-rw-r--r--module/vcomponent/parse/component.scm143
-rw-r--r--module/vcomponent/recurrence/generate-alt.scm5
-rw-r--r--tests/prop.scm4
-rw-r--r--tests/vcomponent.scm6
14 files changed, 265 insertions, 377 deletions
diff --git a/.gitignore b/.gitignore
index fe87e20e..a6b94637 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-parse
*.x
html
diff --git a/module/datetime.scm b/module/datetime.scm
index 0cca216b..b2a3d38e 100644
--- a/module/datetime.scm
+++ b/module/datetime.scm
@@ -188,7 +188,7 @@
[else (error "Object not a date, time, or datetime object ~a" date/-time)]))
(define-public (as-time date/-time)
- (cond [(datetime? date/-time) (get-time% (get-datetime date/-time))]
+ (cond [(datetime? date/-time) (get-time% date/-time)]
[(date? date/-time) (time)]
[(time? date/-time) date/-time]
[else (error "Object not a date, time, or datetime object ~a" date/-time)]))
@@ -219,10 +219,8 @@
(= (second a) (second b))))
(define-public (datetime= a b)
- (let ((a (get-datetime a))
- (b (get-datetime b)))
- (and (date= (get-date a) (get-date b))
- (time= (get-time% a) (get-time% b)))))
+ (and (date= (get-date a) (get-date b))
+ (time= (get-time% a) (get-time% b))))
(define-many define-public
(date=?) date=
@@ -276,18 +274,14 @@
(time< a b)))
(define-public (datetime< a b)
- (let ((a (get-datetime a))
- (b (get-datetime b)))
- (if (date= (get-date a) (get-date b))
- (time< (get-time% a) (get-time% b))
- (date< (get-date a) (get-date b)))))
+ (if (date= (get-date a) (get-date b))
+ (time< (get-time% a) (get-time% b))
+ (date< (get-date a) (get-date b))))
(define-public (datetime<= a b)
- (let ((a (get-datetime a))
- (b (get-datetime b)))
- (if (date= (get-date a) (get-date b))
- (time<= (get-time% a) (get-time% b))
- (date<= (get-date a) (get-date b)))))
+ (if (date= (get-date a) (get-date b))
+ (time<= (get-time% a) (get-time% b))
+ (date<= (get-date a) (get-date b))))
(define-public (date/-time< a b)
(datetime< (as-datetime a) (as-datetime b)))
@@ -560,15 +554,13 @@
;; NOTE that base is re-normalized, but change isn't. This is due to base
;; hopefully being a real date, but change just being a difference.
(define-public (datetime+ base change)
- (let (; (base (get-datetime base))
- )
- (let* ((time overflow (time+ (get-time% base) (get-time% change))))
- (datetime date: (date+ (get-date base)
- (get-date change)
- (date day: overflow))
- time: time
- tz: (get-timezone base)
- ))))
+ (let* ((time overflow (time+ (get-time% base) (get-time% change))))
+ (datetime date: (date+ (get-date base)
+ (get-date change)
+ (date day: overflow))
+ time: time
+ tz: (get-timezone base)
+ )))
;; (define (datetime->srfi-19-date date)
;; ((@ (srfi srfi-19) make-date)
@@ -658,16 +650,14 @@
(day = (- 1)))))
-(define-public (datetime-difference end* start*)
+(define-public (datetime-difference end start)
;; NOTE Makes both start and end datetimes in the current local time.
- (let ((end (get-datetime end*))
- (start (get-datetime start*)))
- (let* ((fixed-time overflow (time- (get-time% end)
- (get-time% start))))
- (datetime date: (date-difference (date- (get-date end)
- (date day: overflow))
- (get-date start))
- time: fixed-time))))
+ (let* ((fixed-time overflow (time- (get-time% end)
+ (get-time% start))))
+ (datetime date: (date-difference (date- (get-date end)
+ (date day: overflow))
+ (get-date start))
+ time: fixed-time)))
diff --git a/module/datetime/util.scm b/module/datetime/util.scm
index 910c42d3..d310992c 100644
--- a/module/datetime/util.scm
+++ b/module/datetime/util.scm
@@ -157,9 +157,8 @@
str)))
(define*-public (datetime->string datetime optional: (fmt "~Y-~m-~dT~H:~M:~S") key: allow-unknown?)
- (define dt (get-datetime datetime))
- (define date (get-date dt))
- (define time ((@ (datetime) get-time%) dt))
+ (define date (get-date datetime))
+ (define time ((@ (datetime) get-time%) datetime))
(with-output-to-string
(lambda ()
(fold (lambda (token state)
diff --git a/module/entry-points/benchmark.scm b/module/entry-points/benchmark.scm
index 4843a80a..701d786b 100644
--- a/module/entry-points/benchmark.scm
+++ b/module/entry-points/benchmark.scm
@@ -3,15 +3,15 @@
:use-module (ice-9 getopt-long)
:use-module (util)
- :use-module (vcomponent)
+ :use-module (util app)
)
(define opt-spec
- '((file (value #t) (single-char #\f))))
+ '())
(define (main args)
(define opts (getopt-long args opt-spec))
- (cond [(option-ref opts 'file #f) => (compose load-calendars* list)]
- [else (load-calendars)]))
+ (write (getf 'calendars app: (current-app)))
+)
diff --git a/module/entry-points/html.scm b/module/entry-points/html.scm
index e03e5907..1237f628 100644
--- a/module/entry-points/html.scm
+++ b/module/entry-points/html.scm
@@ -66,4 +66,7 @@
[(table)
(html-table-main count start)]
[else
- (error "Unknown html style: ~a" style)]))
+ (error "Unknown html style: ~a" style)])
+
+ ((@ (util time) report-time!) "all done")
+ )
diff --git a/module/output/html.scm b/module/output/html.scm
index 3b17d81b..da5c0659 100644
--- a/module/output/html.scm
+++ b/module/output/html.scm
@@ -145,12 +145,17 @@
,(fmt-single-event ev))))
+(define (->string a)
+ (format #f "~a" a))
+
(define (data-attributes event)
(hash-map->list
(match-lambda*
+ [(key (vlines ...)) (list (string->symbol (format #f "data-~a" key))
+ (string-join (map (compose ->string value) vlines) ","))]
[(key vline)
(list (string->symbol (format #f "data-~a" key))
- (format #f "~a" (value vline)))]
+ (->string (value vline)))]
[_ (error "What are you doing‽")])
(attributes event)))
@@ -234,12 +239,12 @@
(make-block
ev `((class
- ,(when (date<? (as-date (get-datetime (attr ev 'DTSTART))) date)
+ ,(when (date<? (as-date (attr ev 'DTSTART)) date)
" continued")
;; TODO all day events usually have the day after as DTEND.
;; So a whole day event the 6 june would have a DTEND of the
;; 7 june.
- ,(when (date<? date (as-date (get-datetime (attr ev 'DTEND))))
+ ,(when (date<? date (as-date (attr ev 'DTEND)))
" continuing"))
(style ,style))))
@@ -354,7 +359,7 @@
,((compose (@ (vcomponent recurrence display) format-recurrence-rule)
(@ (vcomponent recurrence parse) parse-recurrence-rule))
(attr ev 'RRULE))
- ,@(awhen (attr ev 'EXDATE)
+ ,@(awhen (attr* ev 'EXDATE)
(list
", undantaget "
(add-enumeration-punctuation
@@ -371,7 +376,7 @@
'(HOURLY MINUTELY SECONDLY))
(datetime->string d "~e ~b ~k:~M")
(datetime->string d "~e ~b"))))
- it))))
+ (map value it)))))
"."))
(define (format-description ev str)
@@ -783,6 +788,8 @@
(define calendars (getf 'calendars))
(define events (getf 'event-set))
+ ((@ (util time) report-time!) "html start")
+
;; TODO This still doesn't account for PWD, file existing but is of
;; wrong type, html directory existing but static symlink missing,
;; static being a different file type, and probably something else
diff --git a/module/util/exceptions.scm b/module/util/exceptions.scm
index 41efaff5..4673b182 100644
--- a/module/util/exceptions.scm
+++ b/module/util/exceptions.scm
@@ -2,7 +2,8 @@
#:use-module (srfi srfi-1)
#:use-module (util)
#:export (throw-returnable
- catch-multiple))
+ catch-multiple
+ assert))
(define-syntax-rule (throw-returnable symb args ...)
(call/cc (lambda (cont) (throw symb cont args ...))))
@@ -52,3 +53,21 @@
(display (apply (warning-handler) fmt (or args '()))
(current-error-port)))
+
+(define (prettify-tree tree)
+ (cond [(null? tree) '()]
+ [(pair? tree) (cons (prettify-tree (car tree))
+ (prettify-tree (cdr tree)))]
+ [(list? tree) (map prettify-tree tree)]
+ [(and (procedure? tree)
+ (procedure-name tree))
+ => identity]
+ [else tree]))
+
+
+
+(define-macro (assert form)
+ `(unless ,form
+ (throw 'assertion-error "Assertion for ~a failed, ~a"
+ (quote ,form)
+ ((@@ (util exceptions) prettify-tree) ,(cons 'list form)))))
diff --git a/module/vcomponent/base.scm b/module/vcomponent/base.scm
index e0d7d11e..994ac197 100644
--- a/module/vcomponent/base.scm
+++ b/module/vcomponent/base.scm
@@ -11,8 +11,9 @@
;; The <vline> type is a bit to many times refered to as a attr ptr.
(define-record-type <vline>
- (make-vline% value parameters)
+ (make-vline% key value parameters)
vline?
+ (key vline-key)
(value get-vline-value set-vline-value!)
(parameters get-vline-parameters)
;; TODO Add slot for optional source object, containing
@@ -21,8 +22,10 @@
;; - source string, before value parsing.
)
-(define*-public (make-vline value #:optional (ht (make-hash-table)))
- (make-vline% value ht))
+(export vline-key)
+
+(define*-public (make-vline key value #:optional (ht (make-hash-table)))
+ (make-vline% key value ht))
(define-record-type <vcomponent>
(make-vcomponent% type children parent attributes)
@@ -53,6 +56,7 @@
(set-component-children! parent (cons child (children parent)))
(set-component-parent! child parent))
+;; TODO this doesn't handle multi-valued items
(define* (get-attribute-value component key #:optional default)
(cond [(hashq-ref (get-component-attributes component)
key #f)
@@ -67,7 +71,7 @@
(let ((ht (get-component-attributes component)))
(cond [(hashq-ref ht key #f)
=> (lambda (vline) (set-vline-value! vline value))]
- [else (hashq-set! ht key (make-vline value))])))
+ [else (hashq-set! ht key (make-vline key value))])))
(define-public (set-vline! component key vline)
(hashq-set! (get-component-attributes component)
@@ -81,10 +85,19 @@
get-vline-value set-vline-value!))
;; vcomponent x (or str symb) → vline
-(define-public (attr* component attr)
+(define (get-attr* component attr)
(hashq-ref (get-component-attributes component)
(as-symb attr)))
+(define (set-attr*! component key value)
+ (hashq-set! (get-component-attributes component)
+ (as-symb key) value))
+
+(define-public attr*
+ (make-procedure-with-setter
+ get-attr*
+ set-attr*!))
+
;; vcomponent x (or str symb) → value
(define (get-attr component key)
(get-attribute-value component (as-symb key) #f))
@@ -122,7 +135,8 @@
(map car (hash-map->list cons (get-component-attributes component))))
(define (copy-vline vline)
- (make-vline (get-vline-value vline)
+ (make-vline (vline-key vline)
+ (get-vline-value vline)
;; TODO deep-copy on properties?
(get-vline-parameters vline)))
@@ -132,7 +146,9 @@
(parent component)
;; attributes
(alist->hashq-table
- (hash-map->list (lambda (key value) (cons key (copy-vline value)))
+ (hash-map->list (lambda (key value) (cons key (if (list? value)
+ (map copy-vline value)
+ (copy-vline value))))
(get-component-attributes component)))))
(define-public (extract field)
diff --git a/module/vcomponent/datetime.scm b/module/vcomponent/datetime.scm
index 44776516..68909809 100644
--- a/module/vcomponent/datetime.scm
+++ b/module/vcomponent/datetime.scm
@@ -66,9 +66,9 @@ Event must have the DTSTART and DTEND attribute set."
(date-max start-date
(attr e 'DTSTART)))
(datetime-difference (datetime-min (datetime date: (date+ end-date (date day: 1)))
- (get-datetime (attr e 'DTEND)))
+ (attr e 'DTEND))
(datetime-max (datetime date: start-date)
- (get-datetime (attr e 'DTSTART))))))
+ (attr e 'DTSTART)))))
;; Returns the length of the part of @var{e} which is within the day
;; starting at the time @var{start-of-day}.
@@ -76,8 +76,8 @@ Event must have the DTSTART and DTEND attribute set."
;; to a datetime to allow for more explicit TZ handling?
(define-public (event-length/day date e)
;; TODO date= > 2 elements
- (let ((start (get-datetime (attr e 'DTSTART)))
- (end (get-datetime (attr e 'DTEND))))
+ (let ((start (attr e 'DTSTART))
+ (end (attr e 'DTEND)))
(cond [(and (date= (as-date start)
(as-date end))
(date= (as-date start)
diff --git a/module/vcomponent/parse.scm b/module/vcomponent/parse.scm
index b5bb17e9..9e5be8a3 100644
--- a/module/vcomponent/parse.scm
+++ b/module/vcomponent/parse.scm
@@ -1,320 +1,26 @@
+;;; Commentary:
+;; Code for parsing vdir's and icalendar files.
+;; This module handles the finding of files, while
+;; (vcomponent parse component) handles reading data from icalendar files.
+;;; Code:
+
(define-module (vcomponent parse)
- :use-module ((rnrs io ports) :select (get-u8))
:use-module (rnrs bytevectors)
:use-module (srfi srfi-1)
- :use-module (srfi srfi-9)
- :use-module (datetime)
- :use-module (datetime util)
- :use-module (srfi srfi-26)
+
:use-module ((ice-9 hash-table) :select (alist->hash-table))
:use-module ((ice-9 rdelim) :select (read-line))
- :use-module ((ice-9 textual-ports) :select (unget-char))
:use-module ((ice-9 ftw) :select (scandir ftw))
:use-module (util)
:use-module (util time)
- :use-module (util strbuf)
:use-module (util exceptions)
:use-module (vcomponent base)
- :use-module (vcomponent datetime)
- :use-module (datetime util)
- )
-
-(use-modules ((rnrs base) #:select (assert)))
-
-
-
-
-(define-record-type <parse-ctx>
- (make-parse-ctx% filename row col ctx line-key param-key param-table)
- parse-ctx?
- (filename get-filename) ; string
- (row get-row set-row!) ; [0, ]
- (col get-col set-col!) ; [1, )
- (ctx get-ctx set-ctx!) ; '(key value param-name param-value escape)
- (line-key get-line-key set-line-key!) ; string
- (param-key get-param-key set-param-key!) ; string
- (param-table get-param-table set-param-table!) ; hash-map
- )
-
-(define (make-parse-ctx filename)
- (make-parse-ctx% filename 1 0 'key
- #f #f (make-hash-table)))
-
-(define (increment-column! ctx)
- (set-col! ctx (1+ (get-col ctx))))
-(define (increment-row! ctx)
- (set-col! ctx 0)
- (set-row! ctx (1+ (get-row ctx))))
-
-(define (ctx-dump-strings! ctx)
- (set-line-key! ctx "")
- (set-param-key! ctx "")
- ;; (set-param-table! ctx (make-hash-table))
+ :use-module (vcomponent parse component)
+ :re-export (parse-calendar)
)
-(define-macro (with-vline-tz object . body)
- `(let-env ((TZ (and=> (prop ,object 'TZID) car)))
- ,@body))
-
-
-
-
-(define (fold-proc ctx c)
- ;; First extra character optional read is to get the \n if our line
- ;; ended with \r\n. Secound read is to get the first character of the
- ;; next line. The initial \r which might recide in @var{c} is discarded.
- (let ((pair (cons (if (char=? #\newline (integer->char c))
- c (get-u8 (current-input-port)))
- (get-u8 (current-input-port)))))
- (increment-row! ctx)
- (cond [(not (char=? #\newline (integer->char (car pair))))
- (error "Expected newline after CR")]
-
- ;; The standard (3.4, l. 2675) says that each icalobject must
- ;; end with CRLF. My files however does not. This means that
- ;; an EOF can immideately follow a \n\r pair. But this case is the
- ;; same as that we are at the end of line, so we spoof it and let
- ;; the regular parser loop handle it.
- [(eof-object? (cdr pair))
- 'end-of-line]
-
- ;; Following line begins with a whitespace character,
- ;; meaning that we don't break the logical line here.
- [(memv (integer->char (cdr pair)) '(#\space #\tab))
- (increment-column! ctx) ; since we just read the space
- 'fold]
-
- [else
- ;; TODO check if this failed, and signal a writeback error
- (unget-char (current-input-port)
- (integer->char (cdr pair)))
-
- 'end-of-line])))
-
-(define (handle-value! key vline strbuf)
- (case key
- ;; As far as I can tell the RFC says nothing about special
- ;; encoding for individual fields. It mentieons UTF-8, and
- ;; that transfer encoding should be set in the mime-headers.
- ;; That however seems like a breach of abstractions.
- ;; Currently I allow a CHARSET property on SUMMARY fields,
- ;; since I know that at least www.lysator.liu.se/alma/alma.cgi
- ;; uses it.
- [(SUMMARY)
- (cond [(and=> (prop vline 'CHARSET) car)
- => (lambda (encoding)
- (set! (value vline)
- (strbuf->string strbuf ((@ (rnrs io ports) make-transcoder)
- encoding))))])]
-
- [(DTSTART DTEND RECURRENCE-ID LAST-MODIFIED DTSTAMP EXDATE)
-
- ;; '("Africa/Ceuta" "Europe/Stockholm" "local")
- (let ((tz (or (and=> (prop vline 'TZID) car)
- (and (string= "Z" (string-take-right (value vline) 1)) "UTC"))))
-
- (let ((type (and=> (prop vline 'VALUE) car)))
- (if (or (and=> type (cut string=? <> "DATE-TIME"))
- (string-contains (value vline) "T"))
- (set! (value vline) (parse-ics-datetime (value vline) tz)
- (prop vline 'VALUE) 'DATE-TIME)
- (set! (value vline) (parse-ics-date (value vline))
- (prop vline 'VALUE) 'DATE))))]))
-
-;; Reads a vcomponent from the given port.
-(define-public (parse-calendar port)
- ;; (report-time! "Parsing ~a" port)
- (with-input-from-port port
- (lambda ()
- (let ((component (make-vcomponent))
- (ctx (make-parse-ctx (port-filename port)))
- (strbuf (make-strbuf)))
- (parameterize ((warning-handler
- (lambda (fmt . args)
- (format #f
- "== PARSE WARNING ==
-filename = ~a
-row ~a column ~a ctx = ~a
-~a ; ~a = ... : ...
-~?~%~%"
- (get-filename ctx)
- (get-row ctx) (get-col ctx) (get-ctx ctx)
- (get-line-key ctx) (get-param-key ctx)
- fmt args))))
- (with-throw-handler #t
- (lambda ()
- (while #t
- (let ((c (get-u8 (current-input-port))))
- (cond
-
- ;; End of file
- [(eof-object? c)
- ;; == NOTE ==
- ;; We never check the final line here. But since it
- ;; ALWAYS should be "END:VCOMPONENT", and we do all
- ;; the setup at creation this shouldn't be a problem.
- (let ((component
- (case (get-ctx ctx)
- ;; Line ended before we came here, get the actual root
- ;; component (instead of our virtual one:
- [(key) (car (children component))]
- ;; Line wasn't ended before we get here, so our current
- ;; component is our "actual" root. NOTE that this never
- ;; actually finalizes the root object, which matters if
- ;; if do something with the finalizer below.
- ;; At the time of writing we just set the parent.
- [(value) component]
- [else
- => (lambda (a)
- (scm-error
- 'wrong-type-arg "parse-break"
- (string-append
- "Bad context at end of file. "
- "Expected `key' or `value', got ~a")
- (list a) #f))])))
- ;; == NOTE ==
- ;; This sets to the VCALENDAR, which is correct,
- ;; but the program later squashes together everything
- ;; and drops this information.
- (set! (attr component 'X-HNH-FILENAME) (get-filename ctx)
- (parent component) #f)
- (break component))]
-
- ;; End of line
- [(memv (integer->char c) '(#\return #\newline))
- (case (fold-proc ctx c)
- [(end-of-line)
- (let ((str (strbuf->string strbuf)))
- (cond [(and (eq? 'key (get-ctx ctx))
- (string-null? str))
- ;; I believe that an empty line is against the standard
- ;; in every way. But it's nice to handle it.
- (warning "Unexpected completely empty line")]
-
- [(eq? 'BEGIN (get-line-key ctx))
- (let ((child (make-vcomponent (string->symbol str))))
- (add-child! component child)
- (set! component child))]
-
- [(eq? (get-line-key ctx) 'END)
-
- ;; Ensure that we have a DTEND
- ;; TODO Objects aren't required to have a DTEND, or a DURATION.
- ;; write fancier code which acknoledges this.
- (when (and (eq? 'VEVENT (type component))
- (not (attr component 'DTEND)))
- (set! (attr component 'DTEND)
- (let ((start (attr component 'DTSTART)))
- ;; p. 54, 3.6.1
- ;; If DTSTART is a date then it's an all
- ;; day event. If DTSTART instead is a
- ;; datetime then the event has a length
- ;; of 0?
- (if (date? start)
- (date+ start (date day: 1))
- (datetime+ start (datetime time: (time hour: 1)))))))
-
- (set! component (parent component))]
-
- [else ; Regular key-value line
- (let ((key (get-line-key ctx))
- (vline (make-vline str (get-param-table ctx))))
- ;; Type specific processing
- (handle-value! key vline strbuf)
-
- ;; From RFC 5545 §3.6.1
- ;; DTEND and DURATION are mutually exclusive
- ;; DTSTART is required to exist while the other two are optional.
-
- ;; Allowed (some) repeated keys
- (if (memv key '(EXDATE ATTENDEE))
- (aif (attr* component key)
- ;; updates the current vline
- ;; NOTE that this discards any properties belonging to this object
- ;; TODO a more propper way to do it would be to store multiple vline
- ;; objects for a given key.
- (set! (value it) (cons (value vline) (value it)))
- (begin (mod! (value vline) list)
- (set-vline! component key vline)))
- ;; Keys which aren't allowed to be repeated.
- (begin
- (awhen (attr* component key)
- (warning "Key ~a encountered more than once, overriding old value [~a] with [~a]"
- key (value it) (value vline)))
- (set-vline! component key vline))))
- (set-param-table! ctx (make-hash-table))])
-
- (strbuf-reset! strbuf)
- (ctx-dump-strings! ctx)
- (set-ctx! ctx 'key))]
- [(fold) 'noop] ; Good case, here to catch errors in else
- [else => (lambda (a) (error "Bad return from fold, unexpected" a))])]
-
- ;; Escaped characters
- [(char=? #\\ (integer->char c))
- (case (integer->char (get-u8 (current-input-port)))
- ;; Escape character '\' and escaped token sepparated by a newline
- ;; (since the standard for some reason allows that (!!!))
- ;; We are at least guaranteed that it's a folded line, so just
- ;; unfold it and continue trying to find a token to escape.
- [(#\return #\newline)
- => (lambda (c)
- (case (fold-proc ctx (char->integer c))
- [(end-of-line)
- (throw 'escape-error "ESC before not folded line")]
- [(fold)
- (increment-column! ctx)
- (strbuf-append! strbuf (get-u8 (current-input-port)))]))]
-
- [(#\n #\N) (strbuf-append! strbuf (char->integer #\newline))]
- [(#\; #\, #\\) => (lambda (c) (strbuf-append! strbuf (char->integer c)))]
- [else => (lambda (c) (warning "Non-escapable character: ~a" c))])
- (increment-column! ctx)]
-
- ;; Delimiter between param key and param value
- [(and (eq? (get-ctx ctx) 'param-name)
- (char=? #\= (integer->char c)))
- (set-param-key! ctx (string->symbol (strbuf->string strbuf)))
- (strbuf-reset! strbuf)
- (set-ctx! ctx 'param-value)]
-
- ;; Delimiter between parameters (;), or between
- ;; "something" and attribute value (:)
- [(and (memv (integer->char c) '(#\: #\;))
- (memv (get-ctx ctx) '(param-value key)))
- (case (get-ctx ctx)
- [(param-value)
- (hashq-set! (get-param-table ctx)
- (get-param-key ctx)
- (strbuf->string strbuf))
- (strbuf-reset! strbuf)]
- [(key)
- (set-line-key! ctx (string->symbol (strbuf->string strbuf)))
- (strbuf-reset! strbuf)])
-
- (set-ctx! ctx (case (integer->char c)
- [(#\:) 'value]
- [(#\;) 'param-name]))]
-
- ;; Regular character
- [else
- (strbuf-append! strbuf c)
- (increment-column! ctx)]))))
-
- (lambda _
- ;; display is atomic, format isn't
- (display
- (format #f
- "== PARSE ERROR ==
-filename = ~a
-row ~a column ~a ctx = ~a
-~a ; ~a = ... : ...~%~%"
- (get-filename ctx)
- (get-row ctx) (get-col ctx) (get-ctx ctx)
- (get-line-key ctx) (get-param-key ctx))))))))))
-
@@ -337,6 +43,7 @@ row ~a column ~a ctx = ~a
(define-values (events other) (partition (lambda (e) (eq? 'VEVENT (type e)))
(children item)))
+
;; (assert (eq? 'VCALENDAR (type calendar)))
(assert (eq? 'VCALENDAR (type item)))
@@ -386,15 +93,17 @@ row ~a column ~a ctx = ~a
;; return
calendar)
(make-vcomponent)
- ((@ (ice-9 threads) par-map) (lambda (fname)
- (let ((fullname (/ path fname)))
- (let ((cal (call-with-input-file fullname
- parse-calendar)))
- (set! (attr cal 'COLOR) color
- (attr cal 'NAME) name)
- cal)))
- (scandir path (lambda (s) (and (not (string= "." (string-take s 1)))
- (string= "ics" (string-take-right s 3))))))))))
+ (map #; (@ (ice-9 threads) par-map)
+ (lambda (fname)
+ (let ((fullname (/ path fname)))
+ (let ((cal (call-with-input-file fullname
+ parse-calendar)))
+ (set! (attr cal 'COLOR) color
+ (attr cal 'NAME) name
+ (attr cal 'X-HNH-FILENAME) fullname)
+ cal)))
+ (scandir path (lambda (s) (and (not (string= "." (string-take s 1)))
+ (string= "ics" (string-take-right s 3))))))))))
;; Parse a vdir or ics file at the given path.
(define-public (parse-cal-path path)
diff --git a/module/vcomponent/parse/component.scm b/module/vcomponent/parse/component.scm
new file mode 100644
index 00000000..565c129d
--- /dev/null
+++ b/module/vcomponent/parse/component.scm
@@ -0,0 +1,143 @@
+(define-module (vcomponent parse component)
+ :use-module (util)
+ :use-module (util exceptions)
+ :use-module ((ice-9 rdelim) :select (read-line))
+ :use-module (vcomponent base)
+ :use-module (datetime)
+ :use-module (srfi srfi-1)
+ :use-module (srfi srfi-26)
+ )
+
+(define-public (parse-calendar port)
+ (parse (map tokenize (read-file port))))
+
+;; port → (list string)
+(define (read-file port)
+ (let loop ((done '()))
+ (let ((line (read-line port)))
+ (if (eof-object? line)
+ (reverse! done)
+ (let ((line (string-trim-right line)))
+ (loop
+ (if (char=? #\space (string-ref line 0))
+ (cons (string-append (car done)
+ (string-drop line 1))
+ (cdr done))
+ (cons line done))))))))
+
+;; (list string) → (list (key kv ... value))
+(define (tokenize line)
+ (define colon-idx (string-index line #\:))
+ (define semi-idxs
+ (let loop ((idx 0))
+ (aif (string-index line #\; idx colon-idx)
+ (cons it (loop (1+ it)))
+ (list colon-idx (string-length line)))))
+ (map (lambda (start end)
+ (substring line (1+ start) end))
+ (cons -1 semi-idxs)
+ semi-idxs))
+
+;; params could be made optional, with an empty hashtable as default
+(define (build-vline key value params)
+ (case key
+ [(DTSTART DTEND RECURRENCE-ID LAST-MODIFIED DTSTAMP EXDATE)
+
+ ;; '("Africa/Ceuta" "Europe/Stockholm" "local")
+ (let ((tz (or (hashq-ref params 'TZID)
+ (and (string= "Z" (string-take-right value 1)) "UTC"))))
+
+ (let ((type (hashq-ref params 'VALUE)))
+ (if (or (and=> type (cut string=? <> "DATE-TIME"))
+ (string-index value #\T))
+ ;; we move all parsed datetimes to local time here. This
+ ;; gives a MASSIVE performance boost over calling get-datetime
+ ;; in all procedures which want to guarantee local time for proper calculations.
+ ;; 20s vs 70s runtime on my laptop.
+ (let ((datetime (parse-ics-datetime value tz)))
+ (hashq-set! params 'VALUE 'DATE-TIME)
+ (values (make-vline key (get-datetime datetime) params)
+ (make-vline (symbol-append 'X-ORIGINAL- key) datetime params)))
+ (begin (hashq-set! params 'VALUE 'DATE)
+ (make-vline key (parse-ics-date value) params)))))]
+
+ [else
+ (make-vline key
+ (list->string
+ (let loop ((rem (string->list value)))
+ (if (null? rem)
+ '()
+ (if (char=? #\\ (car rem))
+ (case (cadr rem)
+ [(#\n #\N) (cons #\newline (loop (cddr rem)))]
+ [(#\; #\, #\\) => (lambda (c) (cons c (loop (cddr rem))))]
+ [else => (lambda (c) (warning "Non-escapable character: ~a" c)
+ (loop (cddr rem)))])
+ (cons (car rem) (loop (cdr rem)))))))
+ params)]))
+
+;; (parse-itemline '("DTEND" "20200407T130000"))
+;; => DTEND
+;; => "20200407T130000"
+;; => #<hash-table 7f76b5f82a60 0/31>
+(define (parse-itemline itemline)
+ (define key (string->symbol (car itemline)))
+ (define parameters (make-hash-table))
+ (let loop ((rem (cdr itemline)))
+ (if (null? (cdr rem))
+ (values key (car rem) parameters )
+ (let* ((kv (car rem))
+ (idx (string-index kv #\=)))
+ (hashq-set! parameters (string->symbol (substring kv 0 idx))
+ (substring kv (1+ idx)))
+ (loop (cdr rem))))))
+
+
+;; (list (key kv ... value)) → <vcomponent>
+(define (parse lst)
+ (let loop ((lst lst)
+ (stack '()))
+ (if (null? lst)
+ stack
+ (let ((head (car lst)))
+ (cond [(string=? "BEGIN" (car head))
+ (loop (cdr lst) (cons (make-vcomponent (string->symbol (cadr head))) stack))]
+ [(string=? "END" (car head))
+
+ ;; TODO This is an ugly hack until the rest of the code is updated
+ ;; to work on events without an explicit DTEND attribute.
+ (when (and (eq? (type (car stack)) 'VEVENT)
+ (not (attr (car stack) 'DTEND)))
+ (set! (attr (car stack) 'DTEND)
+ (let ((start (attr (car stack) 'DTSTART)))
+ ;; p. 54, 3.6.1
+ ;; If DTSTART is a date then it's an all
+ ;; day event. If DTSTART instead is a
+ ;; datetime then the event has a length
+ ;; of 0?
+ (if (date? start)
+ (date+ start (date day: 1))
+ (datetime+ start (datetime time: (time hour: 1)))))))
+
+ (loop (cdr lst)
+ (if (null? (cdr stack))
+ ;; return
+ (car stack)
+ (begin (add-child! (cadr stack) (car stack))
+ (cdr stack))))]
+ [else
+ (let* ((key value params (parse-itemline head)))
+ (call-with-values (lambda () (build-vline key value params))
+ (lambda vlines
+ (for vline in vlines
+ (define key (vline-key vline))
+
+ ;; Which types are allowed to be given multiple times
+ (if (memv (vline-key vline) '(EXDATE ATTENDEE))
+ (aif (attr* (car stack) key)
+ (set! (attr* (car stack) key) (cons vline it))
+ (set! (attr* (car stack) key) (list vline)))
+ ;; else
+ (set! (attr* (car stack) key) vline))))))
+
+ (loop (cdr lst) stack)])))))
diff --git a/module/vcomponent/recurrence/generate-alt.scm b/module/vcomponent/recurrence/generate-alt.scm
index d48e471d..c48a6c82 100644
--- a/module/vcomponent/recurrence/generate-alt.scm
+++ b/module/vcomponent/recurrence/generate-alt.scm
@@ -1,6 +1,7 @@
(define-module (vcomponent recurrence generate-alt)
:export (generate-recurrence-set)
:use-module (util)
+ :use-module (util exceptions)
:use-module (srfi srfi-1)
:use-module (srfi srfi-26)
:use-module (srfi srfi-41)
@@ -278,7 +279,9 @@
;; 3.8.5.1 exdate are evaluated AFTER rrule (and rdate)
(let ((date-stream (stream-remove
- (cut member <> (or (attr event 'EXDATE) '()))
+ (aif (attr* event 'EXDATE)
+ (cut member <> (map value it))
+ (const #f))
(generate-posibilities rrule (attr event 'DTSTART))
;; TODO ideally I should merge the limited recurrence set
;; with the list of rdates here. However, I have never
diff --git a/tests/prop.scm b/tests/prop.scm
index a302d790..a178170d 100644
--- a/tests/prop.scm
+++ b/tests/prop.scm
@@ -3,9 +3,9 @@
((util) sort*))
(define v (call-with-input-string
- "BEGIN:VCOMPONENT
+ "BEGIN:DUMMY
KEY;A=1;B=2:Some text
-END:VCOMPONENT"
+END:DUMMY"
parse-calendar))
(test-equal '("1") (prop (attr* v 'KEY) 'A))
diff --git a/tests/vcomponent.scm b/tests/vcomponent.scm
index c64f1a9b..7a392e9e 100644
--- a/tests/vcomponent.scm
+++ b/tests/vcomponent.scm
@@ -2,11 +2,11 @@
((vcomponent) parse-calendar))
(define ev (call-with-input-string
- "BEGIN:VEVENT
+ "BEGIN:DUMMY
KEY:value
-END:VEVENT"
+END:DUMMY"
parse-calendar))
-(test-assert (eq? #f (attr ev 'MISSING)) )
+(test-assert (eq? #f (attr ev 'MISSING)))
(test-assert (attr ev 'KEY))
(test-equal "value" (attr ev 'KEY))