aboutsummaryrefslogtreecommitdiff
path: root/module/vcomponent
diff options
context:
space:
mode:
Diffstat (limited to 'module/vcomponent')
-rw-r--r--module/vcomponent/base.scm52
-rw-r--r--module/vcomponent/config.scm4
-rw-r--r--module/vcomponent/control.scm2
-rw-r--r--module/vcomponent/create.scm121
-rw-r--r--module/vcomponent/data-stores/caldav.scm270
-rw-r--r--module/vcomponent/data-stores/common.scm43
-rw-r--r--module/vcomponent/data-stores/file.scm32
-rw-r--r--module/vcomponent/data-stores/meta.scm29
-rw-r--r--module/vcomponent/data-stores/sqlite.scm186
-rw-r--r--module/vcomponent/data-stores/vdir.scm87
-rw-r--r--module/vcomponent/datetime.scm6
-rw-r--r--module/vcomponent/datetime/output.scm24
-rw-r--r--module/vcomponent/formats/common/types.scm10
-rw-r--r--module/vcomponent/formats/ical.scm17
-rw-r--r--module/vcomponent/formats/ical/output.scm11
-rw-r--r--module/vcomponent/formats/ical/parse.scm17
-rw-r--r--module/vcomponent/formats/ical/types.scm4
-rw-r--r--module/vcomponent/formats/sxcal.scm16
-rw-r--r--module/vcomponent/formats/vdir/parse.scm6
-rw-r--r--module/vcomponent/formats/vdir/save-delete.scm12
-rw-r--r--module/vcomponent/formats/xcal.scm27
-rw-r--r--module/vcomponent/formats/xcal/output.scm37
-rw-r--r--module/vcomponent/formats/xcal/parse.scm210
-rw-r--r--module/vcomponent/formats/xcal/types.scm18
-rw-r--r--module/vcomponent/recurrence/display/en.scm4
-rw-r--r--module/vcomponent/recurrence/display/sv.scm4
-rw-r--r--module/vcomponent/recurrence/internal.scm15
-rw-r--r--module/vcomponent/util/instance.scm7
-rw-r--r--module/vcomponent/util/instance/methods.scm30
-rw-r--r--module/vcomponent/util/parse-cal-path.scm6
-rw-r--r--module/vcomponent/validate.scm16
31 files changed, 1106 insertions, 217 deletions
diff --git a/module/vcomponent/base.scm b/module/vcomponent/base.scm
index b62d45c2..472c5074 100644
--- a/module/vcomponent/base.scm
+++ b/module/vcomponent/base.scm
@@ -15,7 +15,9 @@
vcomponent?
children type parent
- add-child! remove-child!
+ reparent!
+ abandon!
+ orphan!
delete-property!
prop* prop
@@ -28,6 +30,7 @@
parameters
properties
+ copy-as-orphan
copy-vcomponent
x-property?
internal-field?
@@ -75,40 +78,42 @@
(make-vline% key value ht))
(define-record-type <vcomponent>
- (make-vcomponent% type children parent properties)
+ (make-vcomponent% type children properties)
vcomponent?
(type type)
(children children set-component-children!)
- (parent get-component-parent set-component-parent!)
(properties get-component-properties))
((@ (srfi srfi-9 gnu) set-record-type-printer!)
<vcomponent>
(lambda (c p)
- (format p "#<<vcomponent> ~a, len(child)=~a, parent=~a>~%"
+ (format p "#<<vcomponent> ~a, len(child)=~a>"
(type c)
(length (children c))
- (and=> (get-component-parent c) type))))
+ )))
-;; TODO should this also update the parent
-(define parent
- (make-procedure-with-setter
- get-component-parent set-component-parent!))
+
+(define parent% (make-object-property))
+(define (parent x) (parent% x))
(define* (make-vcomponent optional: (type 'VIRTUAL))
- (make-vcomponent% type '() #f (make-hash-table)))
+ (make-vcomponent% type '() (make-hash-table)))
-(define (add-child! parent child)
+;; TODO should this be renamed to `adopt!'? Adopting a child better implies
+;; that the old parent should no longer be considered its parent.
+(define (reparent! parent child)
(set-component-children! parent (cons child (children parent)))
- (set-component-parent! child parent))
+ (set! (parent% child) parent))
-(define (remove-child! parent-component child)
- (unless (eq? parent-component (parent child))
- (scm-error
- 'wrong-type-arg "remove-child!" "Child doesn't belong to parent"
- (list parent-component child) #f))
+(define (abandon! parent-component child)
(set-component-children! parent-component (delq1! child (children parent-component)))
- (set-component-parent! child #f))
+ (when (eq? parent-component (parent% child))
+ (orphan! child)))
+
+;; TODO should this exist? It's really weird to remove our reference to our
+;; parent, without the parent removing their reference to us.
+(define (orphan! child)
+ (set! (parent% child) #f))
;;; TODO key=DTSTART, (date? value) => #t
;;; KRÄVER att (props vline 'VALUE) <- "DATE"
@@ -194,12 +199,10 @@
;; TODO deep-copy on parameters?
(get-vline-parameters vline)))
-(define (copy-vcomponent component)
+(define (copy-as-orphan component)
(make-vcomponent%
(type component)
- ;; TODO deep copy?
(children component)
- (parent component)
;; properties
(alist->hashq-table
(hash-map->list (lambda (key value)
@@ -208,6 +211,13 @@
(copy-vline value))))
(get-component-properties component)))))
+
+(define (copy-vcomponent component)
+ (let ((ev (copy-as-orphan component)))
+ (when (parent component)
+ (reparent! (parent component) ev))
+ ev))
+
(define (extract field)
(lambda (e) (prop e field)))
diff --git a/module/vcomponent/config.scm b/module/vcomponent/config.scm
index b2598207..3bc51557 100644
--- a/module/vcomponent/config.scm
+++ b/module/vcomponent/config.scm
@@ -4,13 +4,13 @@
:use-module (calp util config))
(define-config calendar-files '()
- description: (_ "Which files to parse. Takes a list of paths or a single string which will be globbed.")
+ description: (G_ "Which files to parse. Takes a list of paths or a single string which will be globbed.")
pre: (lambda (v)
(cond [(list? v) v]
[(string? v) ((@ (glob) glob) v)]
[else #f])))
(define-config default-calendar ""
- description: (_ "Default calendar to use for operations. Set to empty string to unset")
+ description: (G_ "Default calendar to use for operations. Set to empty string to unset")
pre: (ensure string?))
diff --git a/module/vcomponent/control.scm b/module/vcomponent/control.scm
index 0869543d..19a6fa18 100644
--- a/module/vcomponent/control.scm
+++ b/module/vcomponent/control.scm
@@ -26,7 +26,7 @@
;; TODO what is this even used for?
(define-syntax with-replaced-properties
(syntax-rules ()
- [(_ (component (key val) ...)
+ [(G_ (component (key val) ...)
body ...)
(let ((htable (make-hash-table 10)))
diff --git a/module/vcomponent/create.scm b/module/vcomponent/create.scm
new file mode 100644
index 00000000..374da8b4
--- /dev/null
+++ b/module/vcomponent/create.scm
@@ -0,0 +1,121 @@
+(define-module (vcomponent create)
+ :use-module (vcomponent base)
+ :use-module ((srfi srfi-1) :select (last drop-right car+cdr))
+ :use-module (srfi srfi-9)
+ :use-module (srfi srfi-9 gnu)
+ :use-module (srfi srfi-17)
+ :use-module (srfi srfi-71)
+ :use-module (srfi srfi-88)
+ :use-module ((ice-9 hash-table) :select (alist->hashq-table))
+ :use-module ((hnh util) :select (kvlist->assq ->))
+ :export (with-parameters
+ as-list
+ vcomponent
+ vcalendar vevent
+ vtimezone standard daylight
+ ))
+
+;; TODO allow parameters and list values at same time
+
+
+
+;; Convert a scheme keyword to a symbol suitable for us
+(define (keyword->key keyword)
+ (-> keyword
+ keyword->string
+ string-upcase
+ string->symbol))
+
+(define (symbol-upcase symbol)
+ (-> symbol
+ symbol->string
+ string-upcase
+ string->symbol))
+
+;; Upcase the keys in an association list. Keys must be symbols.
+(define (upcase-keys alist)
+ (map (lambda (pair) (cons (symbol-upcase (car pair))
+ (cdr pair)))
+ alist))
+
+
+
+(define-immutable-record-type <almost-vline>
+ (make-almost-vline parameters value)
+ almost-vline?
+ (parameters almost-vline-parameters)
+ (value almost-vline-value))
+
+(define (almost-vline->vline key almost-vline)
+ (make-vline key
+ (almost-vline-value almost-vline)
+ (almost-vline-parameters almost-vline)))
+
+(define (with-parameters . args*)
+ (define parameters (drop-right args* 1))
+ (define value (last args*))
+ (make-almost-vline
+ (-> parameters
+ kvlist->assq
+ upcase-keys
+ alist->hashq-table)
+ value))
+
+
+
+(define-immutable-record-type <list-value>
+ (make-list-value value)
+ list-value?
+ (value list-value-value))
+
+(define (as-list arg)
+ (make-list-value arg))
+
+
+
+(define (vcomponent type . attrs*)
+ (define component (make-vcomponent type))
+ (define attrs*-len (length attrs*))
+ (unless (zero? attrs*-len)
+ (let ((attrs children
+ (if (and (list? (list-ref attrs* (- attrs*-len 1)))
+ (or (= 1 attrs*-len)
+ (not (keyword? (list-ref attrs* (- attrs*-len 2))))))
+ (values (drop-right attrs* 1)
+ (last attrs*))
+ (values attrs* '()))))
+ (for-each (lambda (pair)
+ (let ((key value (car+cdr pair)))
+ (cond
+ ((almost-vline? value)
+ (set! (prop* component key)
+ (almost-vline->vline key value)))
+ ((list-value? value)
+ (set! (prop* component key)
+ (map (lambda (value)
+ (make-vline key value (make-hash-table)))
+ (list-value-value value))))
+ (else
+ (set! (prop component key) value)))))
+ (upcase-keys (kvlist->assq attrs)))
+
+ ;; Attach children
+ (for-each (lambda (child) (reparent! component child))
+ children)))
+
+ component)
+
+(define (vcalendar . attrs)
+ (apply vcomponent 'VCALENDAR attrs))
+
+(define (vevent . attrs)
+ (apply vcomponent 'VEVENT attrs))
+
+(define (vtimezone . attrs)
+ (apply vcomponent 'VTIMEZONE attrs))
+
+(define (standard . attrs)
+ (apply vcomponent 'STANDARD attrs))
+
+(define (daylight . attrs)
+ (apply vcomponent 'DAYLIGHT attrs))
diff --git a/module/vcomponent/data-stores/caldav.scm b/module/vcomponent/data-stores/caldav.scm
new file mode 100644
index 00000000..f9ba61c1
--- /dev/null
+++ b/module/vcomponent/data-stores/caldav.scm
@@ -0,0 +1,270 @@
+(define-module (vcomponent data-stores caldav)
+ )
+
+(use-modules (srfi srfi-71)
+ (srfi srfi-88)
+ (rnrs bytevectors)
+ (rnrs io ports)
+ ((ice-9 binary-ports) :select (call-with-output-bytevector))
+ (web request)
+ (web response)
+ (web client)
+ (web uri)
+ ;; (web http) ;
+ (sxml simple)
+ (oop goops)
+ (vcomponent data-stores common)
+ ((hnh util) :select (->))
+ (web http dav)
+ )
+
+
+
+(define-class <caldav-data-store> (<calendar-data-store>)
+ (host init-keyword: host:
+ getter: host)
+ (user init-keyword: user:
+ getter: user)
+ (calendar-path init-keyword: calendar-path:
+ accessor: calendar-path)
+ (password init-keyword: password:
+ getter: store-password))
+
+
+(define local-uri
+ (case-lambda ((this path)
+ (build-uri 'https
+ host: (host this)
+ path: path))
+ ((this)
+ (build-uri 'https
+ host: (host this)
+ path: (calendar-path this)))))
+
+
+(define* (make-caldav-store key: host user path password)
+ (define store
+ (make <caldav-data-store>
+ host: host
+ user: user
+ password: (string->symbol password)
+ calendar-path: path))
+
+
+ (let* ((principal-path
+ (get-principal (local-uri store "/")
+ password: (store-password store)))
+ (calendar-home-set
+ (get-calendar-home-set (local-uri store principal-path)
+ password: (store-password store)))
+ (calendar-paths
+ (get-calendar-paths (local-uri store calendar-home-set)
+ password: (store-password store))))
+ (set! (calendar-path store)
+ (car calendar-paths)))
+
+ store)
+
+(define-method (write (this <caldav-data-store>) port)
+ (write `(make-caldav-store host: ,(host this)
+ user: ,(user this)
+ calendar-path: ,(calendar-path this)
+ password: ,(store-password this))
+ port))
+
+(define store
+ (make-caldav-store host: "dav.fruux.com"
+ user: "a3298201184"
+ password: "YjMyOTc0NjUwMDk6YXRhc3llanY2MGtu"))
+
+#;
+(define-method (calendar-base (this <caldav-data-store>))
+ (build-uri 'https
+ host: (host this)
+ path: (calendar-path this)))
+
+
+;; (define-method (get-all (this <caldav-data-store>))
+;; )
+
+(define-method (get-by-uid (this <caldav-data-store>)
+ (uid <string>))
+ (let ((uids
+ (dav (local-uri this)
+ method: 'REPORT
+ authorization: `(Basic ,(store-password this))
+ depth: 1
+ body:
+ `(c:calendar-query
+ (@ (xmlns:c ,caldav))
+ (d:prop (@ (xmlns:d "DAV:"))
+ (d:getetag)
+ #; (c:calendar-data)
+ )
+ (c:filter
+ (c:comp-filter
+ (@ (name "VCALENDAR"))
+ (c:comp-filter
+ (@ (name "VEVENT"))
+ (c:prop-filter
+ (@ (name "UID"))
+ (c:text-match (@ (collation "i;octet"))
+ ,uid)))))))))
+ uids))
+
+
+(define-method (search (this <caldav-data-store>)
+ (filter <pair>))
+ (let ((uids
+ (dav (local-uri this)
+ method: 'REPORT
+ authorization: `(Basic ,(store-password this))
+ depth: 1
+ body:
+ `(c:calendar-query
+ (@ (xmlns:c ,caldav))
+ (d:prop (@ (xmlns:d "DAV:"))
+ (d:getetag)
+ (c:calendar-data
+ (c:comp (@ (name "VCALENDAR"))
+ (c:prop (@ (name "PRODID")))))
+ #; (c:calendar-data)
+ )
+ ,filter))))
+ uids))
+
+(define-method (search (this <caldav-data-store>)
+ (filter <string>)
+ (field <string>))
+ (search store
+ `(c:filter
+ (c:comp-filter
+ (@ (name "VCALENDAR"))
+ (c:comp-filter
+ (@ (name "VEVENT"))
+ (c:prop-filter
+ (@ (name ,field))
+ (c:text-match (@ (collation "i;octet"))
+ ,filter)))))))
+
+
+
+(define-method (list-calendars (this <caldav-data-store>))
+ )
+
+
+
+
+(get-principal) ; => "/principals/uid/a3298201184/"
+
+(get-calendar-home-set "/principals/uid/a3298201184/")
+;; => "/calendars/a3298201184/"
+
+(get-calendar-paths "/calendars/a3298201184/")
+;; => ("/calendars/a3298201184/b85ba2e9-18aa-4451-91bb-b52da930e977/")
+
+
+
+(define user "a3298201184")
+(define calendar "b85ba2e9-18aa-4451-91bb-b52da930e977")
+(define password (string->symbol "YjMyOTc0NjUwMDk6YXRhc3llanY2MGtu"))
+(define auth `(Basic ,password))
+
+
+
+
+
+
+(define uri
+ (build-uri 'https
+ host: "dav.fruux.com"
+ path: "/calendars/a3298201184/b85ba2e9-18aa-4451-91bb-b52da930e977/ff95c36c-6ae9-4aa0-b08f-c52d84bf4f26.ics"))
+
+(define-values (response body)
+ (dav uri
+ method: 'GET
+ authorization: auth))
+
+
+
+
+(define-values (response body)
+ (dav uri
+ method: 'PROPFIND
+ authorization: auth
+ body:
+ `(C:supported-collation-set (@ (xmlns:C ,caldav)))))
+
+(define-values (response body)
+ (dav uri
+ method: 'REPORT
+ authorization: auth
+ body:
+ `(C:calendar-query
+ (@ (xmlns:C ,caldav))
+ (D:prop (@ (xmlns:D "DAV:"))
+ (D:getetac)
+ (C:calendar-data))
+ (C:filter
+ (C:comp-filter (@ (name "VCALENDAR"))
+ (C:comp-filter (@ (name "VEVENT"))
+ (C:prop-filter (@ (name "UID"))
+ (C:text-match (@ (collation "i;utf-8"))
+ "Admittansen"))))))))
+
+
+
+
+
+
+(define (add)
+ ;; add new event
+ (http-request 'PUT
+ path: "/path-on-server/<filename>.ics"
+ headers:
+ ((if-none-match "*")
+ (content-type "text/calendar"))
+ body: (ics:serialize event-with-wrapping-calendar)
+ ))
+
+
+(define (get-by-time-range)
+ (http-request 'REPORT
+ path: "/calendar/<calendar-name>"
+ body:
+ ;; See RFC 4791 7.8.1
+ `(*TOP* (*PI* xml "version=\"1.0\" encoding=\"utf-8\"")
+ (C:calendar-query
+ (@ (xmlns:D "DAV:")
+ (xmlns:C "urn:ietf:params:xml:ns:caldav"))
+ (D:prop
+ (D:getetag)
+ (C:calendar-data
+ (C:comp
+ (@ (name "VCALENDAR"))
+ (C:prop (@ (name "VERSION")))
+ (C:prop (@ name "VEVENT")
+ (C:prop (@ (name "SUMMARY")))
+ ...))))
+ (C:filter
+ (C:comp-filter
+ (@ (name "VCALENDAR"))
+ (C:comp-filter
+ (@ (name "VEVENT"))
+ (C:time-range
+ (@ (start ,(datetime->string
+ start
+ "~Y~m~dT~H~M~S~Z"))
+ (end ,(datetime->string
+ end
+ "~Y~m~dT~H~M~S~Z")))))))))))
+
+
+
+
+
+;; (use-modules (curl))
+;; (define c (curl-easy-init))
+;; (curl-easy-setopt c 'url "https://hornquist.se")
+
+;; (curl-easy-perform handle)
diff --git a/module/vcomponent/data-stores/common.scm b/module/vcomponent/data-stores/common.scm
new file mode 100644
index 00000000..2fb4422a
--- /dev/null
+++ b/module/vcomponent/data-stores/common.scm
@@ -0,0 +1,43 @@
+(define-module (vcomponent data-stores common)
+ :use-module ((srfi srfi-88) :select ())
+ :use-module (oop goops)
+ :export (<calendar-data-store>
+ ;; path
+ get-all
+ get-by-uid))
+
+
+(define-class <calendar-data-store> ()
+ ;; (path init-keyword: path:
+ ;; getter: path)
+ )
+
+
+;;; In (calp server routes)
+
+
+
+
+;;; Load - Load store into memero
+;;; Dump - Save store into "disk"
+
+
+(define-method (get-all (this <calendar-data-store>))
+ (scm-error 'not-implemented "get-all"
+ "Get-all is not implemented for ~s"
+ (class-of this)
+ #f))
+
+(define-method (get-by-uid (this <calendar-data-store>) (uid <string>))
+ (scm-error 'not-implemented "get-by-uid"
+ "Get-by-uid is not implemented for ~s"
+ (class-of this)
+ #f))
+
+
+(define-method (color (this <calendar-data-store>))
+ "")
+
+
+(define-method (displayname (this <calendar-data-store>))
+ "")
diff --git a/module/vcomponent/data-stores/file.scm b/module/vcomponent/data-stores/file.scm
new file mode 100644
index 00000000..54676224
--- /dev/null
+++ b/module/vcomponent/data-stores/file.scm
@@ -0,0 +1,32 @@
+(define-module (vcomponent data-stores file)
+ :use-module (oop goops)
+ :use-module ((srfi srfi-88) :select ())
+ :use-module ((calp) :select (prodid))
+ :use-module (vcomponent data-stores common)
+ :use-module ((vcomponent formats ical) :select (serialize deserialize))
+ )
+
+(define-class <file-data-store> (<calendar-data-store>)
+ (path getter: path
+ init-keyword: path:))
+
+(define (make-file-store path)
+ (make <file-store> path: path))
+
+(define-method (get-all (this <file-data-store>))
+ ;; X-WR-CALNAME ⇒ NAME
+ ;; X-WR-CALDESC
+ (call-with-input-file (path this)
+ deserialize))
+
+(define-method (get-by-uid (this <file-data-store>) (uid <string>))
+ #f
+ )
+
+(define-method (queue-write (this <file-data-store>) vcomponent)
+ )
+
+(define-method (flush (this <file-data-store>))
+ (with-atomic-output-to-file (path this)
+ (lambda () (serialize (data this) (current-output-port))))
+ )
diff --git a/module/vcomponent/data-stores/meta.scm b/module/vcomponent/data-stores/meta.scm
new file mode 100644
index 00000000..8ec5f7fd
--- /dev/null
+++ b/module/vcomponent/data-stores/meta.scm
@@ -0,0 +1,29 @@
+;;; Commentary:
+;;; A virtual data store which uses other data stores for its storage.
+;;; Used to merge stores into larger stores
+;;; Code:
+
+(define-module (vcomponent data-stores meta)
+ :use-module (oop goops)
+ :use-module (vcomponent data-stores common)
+ :use-module (srfi srfi-41)
+ :use-module ((srfi srfi-88) :select ())
+ :export ()
+ )
+
+(define-class <meta-data-store> (<calendar-data-store>)
+ (stores accessor: stores
+ init-value: '()
+ init-keyword: stores:))
+
+
+
+(define-method (get-all (this <meta-data-store>))
+ (map get-all (stores this)))
+
+(define-method (get-by-uid (this <meta-data-store>) (uid <string>))
+ (stream-car
+ (stream-append
+ (steam-map (lambda (store) (get-by-uid store uid))
+ (list->stream (stores this)))
+ (stream #f))))
diff --git a/module/vcomponent/data-stores/sqlite.scm b/module/vcomponent/data-stores/sqlite.scm
new file mode 100644
index 00000000..b5b566a8
--- /dev/null
+++ b/module/vcomponent/data-stores/sqlite.scm
@@ -0,0 +1,186 @@
+(define-module (vcomponent data-stores sqlite)
+ :use-module (oop goops)
+ :use-module (vcomponent data-stores common)
+ :use-module (srfi srfi-71)
+ :use-module ((srfi srfi-88) :select ())
+ :use-module (vcomponent)
+ :use-module ((vcomponent formats ical) :prefix #{ical:}#)
+ :use-module ((hnh util) :select (aif))
+ )
+
+
+(catch 'misc-error
+ (lambda ()
+ (use-modules (sqlite3))
+ (provide 'data-store-sqlite))
+ (lambda args 'no-op))
+
+;; (define (sqlite-exec db str)
+;; (display str)
+;; ((@ (sqlite3) sqlite-exec) db str))
+
+(define-class <sqlite-data-store> (<calendar-data-store>)
+ (database accessor: database)
+ (name init-keyword: name: getter: calendar-name)
+ )
+
+(define (initialize-database db)
+ ;;; Setup Content type
+
+ (sqlite-exec db "
+CREATE TABLE IF NOT EXISTS content_type
+( id INTEGER PRIMARY KEY AUTOINCREMENT
+, name TEXT NOT NULL
+)")
+
+ (let ((stmt (sqlite-prepare db "
+INSERT OR IGNORE INTO content_type
+( name ) VALUES ( ? )")))
+ (for-each (lambda (content-type)
+ (sqlite-reset stmt)
+ (sqlite-bind-arguments stmt )
+ (sqlite-step stmt))
+ '("ical"
+ "xcal"
+ "jcal")))
+
+ ;;; Setup calendar
+
+ (sqlite-exec db "
+CREATE TABLE IF NOT EXISTS calendar
+( id INTEGER PRIMARY KEY AUTOINCREMENT
+, name TEXT NOT NULL
+)")
+
+ (sqlite-exec db "
+CREATE TABLE IF NOT EXISTS calendar_properties
+( id INTEGER PRIMARY KEY AUTOINCREMENT
+, calendar INTEGER NOT NULL
+, key TEXT NOT NULL
+, value TEXT NOT NULL
+, FOREIGN KEY (calendar) REFERENCES calendar(id)
+)")
+
+ ;; INSERT INTO calendar_properties (id, key, value)
+ ;; VALUES ( (SELECT id FROM calendar WHERE name = 'Calendar')
+ ;; , 'color'
+ ;; , '#1E90FF')
+
+ ;;; Setup event
+
+ (sqlite-exec db "
+CREATE TABLE IF NOT EXISTS event
+( uid TEXT PRIMARY KEY
+, content_type INTEGER NOT NULL
+, content TEXT NOT NULL
+, calendar INTEGER NOT NULL
+, FOREIGN KEY (content_type) REFERENCES content_type(id)
+, FOREIGN KEY (calendar) REFERENCES calendar(id)
+)")
+
+ (sqlite-exec db "
+CREATE TABLE IF NOT EXISTS event_instances
+( id INTEGER PRIMARY KEY AUTOINCREMENT
+, event TEXT NOT NULL
+, start DATETIME NOT NULL
+, end DATETIME
+, FOREIGN KEY (event) REFERENCES event(uid)
+)")
+
+ (sqlite-exec db "
+CREATE TABLE IF NOT EXISTS event_instances_valid_range
+( start DATETIME NOT NULL
+, end DATETIME NOT NULL
+)")
+ )
+
+(define-method (initialize (this <sqlite-data-store>) args)
+ (next-method)
+ (if (calendar-name this)
+ (set! (database this) (sqlite-open (path this)))
+ (let ((path db-name
+ (aif (string-rindex (path this) #\#)
+ (values (substring (path this) 0 it)
+ (substring (path this) (1+ it)))
+ (scm-error 'misc-error "(initialize <sqlite-data-store>)"
+ "Target calendar name not specified"
+ '() #f))))
+ (set! (database this) (sqlite-open path))
+ (slot-set! this 'name db-name)))
+
+ (initialize-database (database this)))
+
+
+(define-method (get-calendar (this <sqlite-data-store>))
+ (let ((db (database this))
+ (calendar (make-vcomponent 'VCALENDAR)))
+ (let ((stmt (sqlite-prepare db "
+SELECT key, value FROM calendar_properties cp
+LEFT JOIN calendar c ON cp.calendar = c.id
+WHERE c.name = ?
+")))
+ (sqlite-bind-arguments stmt (calendar-name this))
+ (sqlite-fold (lambda (row calendar)
+ (let ((key (vector-ref row 0))
+ (value (vector-ref row 1)))
+ (set-property! calendar
+ (string->symbol key)
+ value))
+ calendar)
+ calendar
+ stmt))
+
+ (let ((stmt (sqlite-prepare db "
+SELECT content_type.name, content
+FROM event
+LEFT JOIN calendar ON event.calendar = calendar.id
+LEFT JOIN content_type ON event.content_type = content_type.id
+WHERE calendar.name = ?
+")))
+ (sqlite-bind-arguments stmt (calendar-name this))
+ (sqlite-fold (lambda (row calendar)
+ (case (string->symbol (vector-ref row 0))
+ ((ical)
+ (add-child! calendar
+ (call-with-input-string (vector-ref row 1)
+ ics:deserialize))
+ calendar)
+ (else
+ (scm-error 'misc-error "(get-calendar <sqlite-data-store>)"
+ "Only iCal data supported, got ~a"
+ (list (vector-ref row 0)) #f)
+ ))
+ )
+ calendar
+ stmt))
+
+ calendar))
+
+
+#;
+(define-method (get-by-uid (this <sqlite-data-store>) (uid <string>))
+ (let ((stmt (sqlite-prepare db "
+SELECT name, content
+FROM event
+LEFT JOIN content_type ON event.content_type = content_type.id
+WHERE event.uid = ?")))
+ (sqlite-bind-arguments stmt uid)
+ (cond ((sqlite-step stmt)
+ => (lambda (record)
+ (case (string->symbol (vector-ref content 0))
+ ((ics)
+ ;; TODO dispatch to higher instance
+ )
+ (else
+ (scm-error 'value-error "get-by-uid"
+ "Can only deserialize ics (uid=~s)"
+ (list uid) #f)))
+
+ ))
+ (else
+ ;; TODO possibly throw no-such-value
+ #f
+ ))
+
+ )
+ )
diff --git a/module/vcomponent/data-stores/vdir.scm b/module/vcomponent/data-stores/vdir.scm
new file mode 100644
index 00000000..f0ed0fdc
--- /dev/null
+++ b/module/vcomponent/data-stores/vdir.scm
@@ -0,0 +1,87 @@
+(define-module (vcomponent data-stores vdir)
+ :use-module (hnh util)
+ :use-module (oop goops)
+ :use-module (vcomponent data-stores common)
+ :use-module (srfi srfi-71)
+ :use-module ((srfi srfi-88) :select ())
+ :use-module (hnh util path)
+ :use-module ((vcomponent formats ical) :select (serialize deserialize))
+ :use-module ((ice-9 ftw) :select (scandir))
+ :export ())
+
+(define-class <vdir-data-store> (<calendar-data-store>)
+ (path getter: path
+ init-keyword: path:)
+ (loaded-calendar accessor: loaded-calendar
+ init-value: #f)
+ (uid-map accessor: uid-map
+ init-value: #f)
+ )
+
+(define (make-vdir-store path)
+ (make <vdir-data-store> path: path))
+
+(define* (get-attribute path key key: dflt)
+ (catch 'system-error
+ (lambda () (call-with-input-file (path-append path key) read-line))
+ (const dflt)))
+
+
+(define-method (get-all (this <vdir-data-store>))
+ (let ((files (scandir (path this) (lambda (item) (string-ci=? "ics" (filename-extension item)))))
+ (calendar (make-vcomponent 'VCALENDAR)))
+ (set! (prop calendar 'NAME) (get-attribute (path this) "displayname")
+ (prop calendar 'COLOR) (get-attribute (path this) "color" "#FFFFFF"))
+ (for-each (lambda (item) (reparent! calendar item))
+ (append-map (lambda (file)
+ (define cal
+ (call-with-input-file (path-append (path this) file)
+ deserialize))
+ (unless (eq? 'VCALENDAR (type cal))
+ (scm-error 'misc-error "get-all<vdir-data-store>"
+ "Unexpected top level component. Expected VCALENDAR, got ~a. In file ~s"
+ (list (type cal) file)))
+ (for-each (lambda (child)
+ (set! (prop child '-X-HNH-FILENAME) file))
+ (children cal))
+ )
+ files))
+ (set! (loaded-calendar this) calendar)
+ calendar))
+
+
+(define-method (get-by-uid (this <vdir-data-store>) (uid <string>))
+ (unless (uid-map this)
+ (let ((cal
+ (or (loaded-calendar this)
+ (get-all this))))
+ (define ht (make-hash-table))
+ (for-each (lambda (ev) (hash-set! ht (uid ev) ev))
+ (children cal))
+ (set! (uid-map this) ht)))
+ (hash-ref m uid #f))
+
+
+(define (wrap-for-output . vcomponents)
+ (let ((calendar (make-vcomponent 'VCALENDAR)))
+ (set! (prop calendar 'VERSION) "2.0"
+ (prop calendar 'PRODID) (prodid)
+ (prop calendar 'CALSCALE) "GREGORIAN")
+ (for-each (lambda (vcomponent) (reparent! calendar vcomponent))
+ vcomponents)
+ calendar))
+
+(define-method (queue-write (this <vdir-data-store>) vcomponent)
+ ;; TODO Multiple components
+ (let ((filename
+ (cond ((prop vcomponent '-X-HNH-FILENAME)
+ => identity)
+ (else
+ (format #f "~a.ics" (prop vcomponent 'UID))))))
+ (with-atomic-output-to-file (path-append (path this) filename)
+ (lambda () (serialize (wrap-for-output vcomponent) (current-output-port))))))
+
+(define-method (flush (this <vdir-data-store>))
+ (sync))
+
+;; (define (get-in-date-interval ))
diff --git a/module/vcomponent/datetime.scm b/module/vcomponent/datetime.scm
index 440ec5fd..a66ba38a 100644
--- a/module/vcomponent/datetime.scm
+++ b/module/vcomponent/datetime.scm
@@ -245,7 +245,7 @@ Event must have the DTSTART and DTEND protperty set."
(prop component 'TZNAME) (zone-entry-format zone-entry)
last-until (zone-entry-until zone-entry)
last-offset new-timespec)
- (add-child! vtimezone component)))]
+ (reparent! vtimezone component)))]
[(zone-entry-rule zone-entry)
=> (lambda (rule-name)
@@ -278,7 +278,7 @@ Event must have the DTSTART and DTEND protperty set."
(awhen (rule->rrule rule)
(set! (prop component 'RRULE) it))
- (add-child! vtimezone component)))
+ (reparent! vtimezone component)))
;; some of the rules might not apply to us since we only
;; started using that rule set later. It's also possible
;; that we stopped using a ruleset which continues existing.
@@ -297,5 +297,5 @@ Event must have the DTSTART and DTEND protperty set."
(prop component 'TZNAME) (zone-entry-format zone-entry)
last-until (zone-entry-until zone-entry)
last-offset (zone-entry-stdoff zone-entry))
- (add-child! vtimezone component))]))
+ (reparent! vtimezone component))]))
vtimezone)
diff --git a/module/vcomponent/datetime/output.scm b/module/vcomponent/datetime/output.scm
index fb3d0478..1226fc44 100644
--- a/module/vcomponent/datetime/output.scm
+++ b/module/vcomponent/datetime/output.scm
@@ -17,27 +17,27 @@
;; [FRR]
;; Part of the sentance "Repeated [every two weeks], except on ~a, ~a & ~a"
;; See everything tagged [FRR]
- `(,(_ "Repeated ")
+ `(,(G_ "Repeated ")
,((@ (vcomponent recurrence display) format-recurrence-rule) (prop ev 'RRULE))
,@(awhen (prop* ev 'EXDATE)
(list
;; See [FRR]
- (_ ", except on ")
+ (G_ ", except on ")
(add-enumeration-punctuation
(map (lambda (d)
;; TODO show year if different from current year
(if (date? d)
;; [FRR] Exception date without time
- (date->string d (_ "~e ~b"))
+ (date->string d (G_ "~e ~b"))
;; NOTE only show time when it's different than the start time?
;; or possibly only when FREQ is hourly or lower.
(if (memv ((@ (vcomponent recurrence internal) freq)
(prop ev 'RRULE))
'(HOURLY MINUTELY SECONDLY))
;; [FRR] Exception date with time
- (datetime->string d (_ "~e ~b ~k:~M"))
+ (datetime->string d (G_ "~e ~b ~k:~M"))
;; [FRR] Exception date without time
- (datetime->string d (_ "~e ~b")))))
+ (datetime->string d (G_ "~e ~b")))))
(map value it)))))
"."))
@@ -52,7 +52,7 @@
;; Warning message for failure to format description.
;; First argument is name of warning/error,
;; second is error arguments
- (warning (_ "~a on formatting description, ~s") err args)
+ (warning (G_ "~a on formatting description, ~s") err args)
str)))
;; Takes an event, and returns a pretty string for the time interval
@@ -64,9 +64,9 @@
=> (lambda (e)
;; start = end, only return one value
(if (date= e (date+ s (date day: 1)))
- (_ "~Y-~m-~d")
- (values (_ "~Y-~m-~d")
- (_ "~Y-~m-~d"))))]
+ (G_ "~Y-~m-~d")
+ (values (G_ "~Y-~m-~d")
+ (G_ "~Y-~m-~d"))))]
;; no end value, just return start
[else (date->string s)]))]
[else ; guaranteed datetime
@@ -74,10 +74,10 @@
(e (prop ev 'DTEND)))
(if e
(let ((fmt-str (if (date= (datetime-date s) (datetime-date e))
- (_ "~H:~M")
+ (G_ "~H:~M")
;; Note the non-breaking space
- (_ "~Y-~m-~d ~H:~M"))))
+ (G_ "~Y-~m-~d ~H:~M"))))
(values fmt-str fmt-str))
;; Note the non-breaking space
- (_ "~Y-~m-~d ~H:~M")))]))
+ (G_ "~Y-~m-~d ~H:~M")))]))
diff --git a/module/vcomponent/formats/common/types.scm b/module/vcomponent/formats/common/types.scm
index a8a923da..fcb2b7b6 100644
--- a/module/vcomponent/formats/common/types.scm
+++ b/module/vcomponent/formats/common/types.scm
@@ -13,7 +13,7 @@
(define (parse-binary props value)
;; p 30
(unless (string=? "BASE64" (hashq-ref props 'ENCODING))
- (warning (_ "Binary field not marked ENCODING=BASE64")))
+ (warning (G_ "Binary field not marked ENCODING=BASE64")))
;; For icalendar no extra whitespace is allowed in a
;; binary field (except for line wrapping). This differs
@@ -25,7 +25,7 @@
(cond
[(string=? "TRUE" value) #t]
[(string=? "FALSE" value) #f]
- [else (warning (_ "~a invalid boolean") value)]))
+ [else (warning (G_ "~a invalid boolean") value)]))
;; CAL-ADDRESS ⇒ uri
@@ -58,7 +58,7 @@
(define (parse-integer props value)
(let ((n (string->number value)))
(unless (integer? n)
- (warning (_ "Non integer as integer")))
+ (warning (G_ "Non integer as integer")))
n))
;; PERIOD
@@ -89,7 +89,7 @@
(case (cadr rem)
[(#\n #\N) (loop (cddr rem) (cons #\newline str) done)]
[(#\; #\, #\\) => (lambda (c) (loop (cddr rem) (cons c str) done))]
- [else => (lambda (c) (warning (_ "Non-escapable character: ~a") c)
+ [else => (lambda (c) (warning (G_ "Non-escapable character: ~a") c)
(loop (cddr rem) str done))])]
[(#\,)
(loop (cdr rem) '() (cons (reverse-list->string str) done))]
@@ -138,5 +138,5 @@
(define (get-parser type)
(or (hashq-ref type-parsers type #f)
- (scm-error 'misc-error "get-parser" (_ "No parser for type ~a")
+ (scm-error 'misc-error "get-parser" (G_ "No parser for type ~a")
(list type) #f)))
diff --git a/module/vcomponent/formats/ical.scm b/module/vcomponent/formats/ical.scm
new file mode 100644
index 00000000..dddca946
--- /dev/null
+++ b/module/vcomponent/formats/ical.scm
@@ -0,0 +1,17 @@
+(define-module (vcomponent formats ical)
+ :use-module ((vcomponent formats ical output)
+ :select (component->ical-string))
+ :use-module ((vcomponent formats ical parse)
+ :select (parse-calendar))
+ :export (serialize
+ deserialize
+ )
+ )
+
+
+(define (serialize component port)
+ (with-output-to-port port
+ (lambda () (component->ical-string component))))
+
+(define (deserialize port)
+ (parse-calendar port))
diff --git a/module/vcomponent/formats/ical/output.scm b/module/vcomponent/formats/ical/output.scm
index da891fa6..57860d2a 100644
--- a/module/vcomponent/formats/ical/output.scm
+++ b/module/vcomponent/formats/ical/output.scm
@@ -16,6 +16,7 @@
:use-module (vcomponent geo)
:use-module (vcomponent formats ical types)
:use-module (vcomponent recurrence)
+ :use-module ((calp) :select (prodid))
:use-module (calp translation)
:autoload (vcomponent util instance) (global-event-object)
:export (component->ical-string
@@ -24,10 +25,6 @@
print-events-in-interval
))
-(define (prodid)
- (format #f "-//hugo//calp ~a//EN"
- (@ (calp) version)))
-
;; Format value depending on key type.
;; Should NOT emit the key.
@@ -96,7 +93,7 @@
(get-writer 'TEXT)]
[else
- (warning (_ "Unknown key ~a") key)
+ (warning (G_ "Unknown key ~a") key)
(get-writer 'TEXT)]))
(catch #t #; 'wrong-type-arg
@@ -168,7 +165,9 @@
;; If we have alternatives, splice them in here.
(cond [(prop component '-X-HNH-ALTERNATIVES)
- => (lambda (alts) (hash-map->list (lambda (_ comp) (component->ical-string comp))
+ => (lambda (alts) (hash-map->list (lambda (_ comp)
+ (unless (eq? component comp)
+ (component->ical-string comp)))
alts))]))
diff --git a/module/vcomponent/formats/ical/parse.scm b/module/vcomponent/formats/ical/parse.scm
index 49f8f101..f0a19ba5 100644
--- a/module/vcomponent/formats/ical/parse.scm
+++ b/module/vcomponent/formats/ical/parse.scm
@@ -14,6 +14,9 @@
:use-module (calp translation)
:export (parse-calendar))
+;;; TODO a few translated strings here contain explicit newlines. Check if that
+;;; is preserved through the translation.
+
(define string->symbol
(let ((ht (make-hash-table 1000)))
(lambda (str)
@@ -124,7 +127,7 @@
(let ((vv (parser params value)))
(when (list? vv)
(scm-error 'parse-error "enum-parser"
- (_ "List in enum field")
+ (G_ "List in enum field")
#f #f))
(let ((v (string->symbol vv)))
(unless (memv v enum)
@@ -160,7 +163,7 @@
(lambda (params value)
(let ((v ((get-parser 'TEXT) params value)))
(unless (= 1 (length v))
- (warning (_ "List in non-list field: ~s") v))
+ (warning (G_ "List in non-list field: ~s") v))
(string-join v ",")))]
;; TEXT, but allow a list
@@ -198,7 +201,7 @@
[(memv key '(REQUEST-STATUS))
(scm-error 'parse-error "build-vline"
- (_ "TODO Implement REQUEST-STATUS")
+ (G_ "TODO Implement REQUEST-STATUS")
#f #f)]
[(memv key '(ACTION))
@@ -233,7 +236,7 @@
(compose car (get-parser 'TEXT))]
[else
- (warning (_ "Unknown key ~a") key)
+ (warning (G_ "Unknown key ~a") key)
(compose car (get-parser 'TEXT))])))
;; If we produced a list create multiple VLINES from it.
@@ -286,7 +289,7 @@
;; ~?
;; source line
;; source file
- (_ "WARNING parse error around ~a
+ (G_ "WARNING parse error around ~a
~?
line ~a ~a~%")
(get-string linedata)
@@ -303,7 +306,7 @@
(if (null? (cdr stack))
;; return
(car stack)
- (begin (add-child! (cadr stack) (car stack))
+ (begin (reparent! (cadr stack) (car stack))
(cdr stack))))]
[else
(let ((key value params (parse-itemline head)))
@@ -341,7 +344,7 @@
;; ~?
;; source line
;; source file
- (_ "ERROR parse error around ~a
+ (G_ "ERROR parse error around ~a
~?
line ~a ~a
Defaulting to string~%")
diff --git a/module/vcomponent/formats/ical/types.scm b/module/vcomponent/formats/ical/types.scm
index 7b6aad2e..768f5098 100644
--- a/module/vcomponent/formats/ical/types.scm
+++ b/module/vcomponent/formats/ical/types.scm
@@ -37,7 +37,7 @@
;; TODO
(define (write-period _ value)
- (warning (_ "PERIOD writer not yet implemented"))
+ (warning (G_ "PERIOD writer not yet implemented"))
(with-output-to-string
(lambda () (write value))))
@@ -94,4 +94,4 @@
(define (get-writer type)
(or (hashq-ref type-writers type #f)
- (error (_ "No writer for type") type)))
+ (error (G_ "No writer for type") type)))
diff --git a/module/vcomponent/formats/sxcal.scm b/module/vcomponent/formats/sxcal.scm
new file mode 100644
index 00000000..c02dbada
--- /dev/null
+++ b/module/vcomponent/formats/sxcal.scm
@@ -0,0 +1,16 @@
+(define-module (vcomponent formats sxcal)
+ :use-module ((vcomponent formats xcal parse)
+ :select (sxcal->vcomponent))
+ :export (serialize deserialize)
+ )
+
+
+(define (serialize component port)
+ (write (serialize/object component) port))
+
+(define (serialize/object component)
+ ;; TODO where is this defined?
+ (vcomponent->sxcal component))
+
+(define (deserialize port)
+ (sxcal->vcomponent port))
diff --git a/module/vcomponent/formats/vdir/parse.scm b/module/vcomponent/formats/vdir/parse.scm
index 46626402..8fe69fc6 100644
--- a/module/vcomponent/formats/vdir/parse.scm
+++ b/module/vcomponent/formats/vdir/parse.scm
@@ -64,9 +64,9 @@
;; by RECURRENCE-ID. As far as I can tell this goes against
;; the standard. Section 3.8.4.4.
(case (length events)
- [(0) (warning (_ "No events in component~%~a")
+ [(0) (warning (G_ "No events in component~%~a")
(prop item '-X-HNH-FILENAME))]
- [(1) (add-child! calendar (car events))]
+ [(1) (reparent! calendar (car events))]
;; two or more
[else
@@ -108,7 +108,7 @@
;; we need to filter duplicates either way.
(map (extract 'RECURRENCE-ID) (cons head rest))
(cons head rest))))
- (add-child! calendar head))])
+ (reparent! calendar head))])
;; return
calendar)
diff --git a/module/vcomponent/formats/vdir/save-delete.scm b/module/vcomponent/formats/vdir/save-delete.scm
index ac520463..d096405e 100644
--- a/module/vcomponent/formats/vdir/save-delete.scm
+++ b/module/vcomponent/formats/vdir/save-delete.scm
@@ -26,16 +26,16 @@
(unless calendar
(scm-error 'wrong-type-arg "save-event"
- (_ "Can only save events belonging to calendars, event uid = ~s")
+ (G_ "Can only save events belonging to calendars, event uid = ~s")
(list (prop event 'UID))
#f))
(unless (eq? 'vdir (prop calendar '-X-HNH-SOURCETYPE))
(scm-error 'wrong-type-arg "save-event"
(string-append
- (_ "Can only save events belonging to vdir calendars.")
+ (G_ "Can only save events belonging to vdir calendars.")
" "
- (_ "Calendar is of type ~s"))
+ (G_ "Calendar is of type ~s"))
(list (prop calendar '-X-HNH-SOURCETYPE))
#f))
@@ -55,10 +55,10 @@
(define calendar (parent event))
(unless (eq? 'vdir (prop calendar '-X-HNH-SOURCETYPE))
(scm-error 'wrong-type-arg "remove-event"
- (string-append (_ "Can only remove events belonging to vdir calendars.")
+ (string-append (G_ "Can only remove events belonging to vdir calendars.")
" "
- (_ "Calendar is of type ~s"))
+ (G_ "Calendar is of type ~s"))
(list (prop calendar '-X-HNH-SOURCETYPE))
#f))
(delete-file (prop event '-X-HNH-FILENAME))
- (remove-child! parent event))
+ (abandon! parent event))
diff --git a/module/vcomponent/formats/xcal.scm b/module/vcomponent/formats/xcal.scm
new file mode 100644
index 00000000..29a1d92f
--- /dev/null
+++ b/module/vcomponent/formats/xcal.scm
@@ -0,0 +1,27 @@
+(define-module (vcomponent formats xcal)
+ :use-module (sxml namespaced)
+ :use-module (sxml namespaced util)
+ :use-module ((vcomponent formats xcal output)
+ :select (vcomponent->sxcal ns-wrap))
+ :use-module ((vcomponent formats xcal parse)
+ :select (sxcal->vcomponent))
+ :use-module ((hnh util) :select (->))
+ :export (serialize deserialize))
+
+
+(define* (serialize component port key: (namespaces '()))
+ (-> (vcomponent->sxcal component)
+ ns-wrap
+ (namespaced-sxml->xml port: port
+ namespaces: namespaces)))
+
+(define (serialize/object component)
+ (call-with-output-string (lambda (p) (serialize component p))))
+
+
+(define* (deserialize port)
+ (-> port
+ xml->namespaced-sxml
+ root-element ; Strip potential *TOP*
+ cadr ; Remove containing icalendar
+ sxcal->vcomponent))
diff --git a/module/vcomponent/formats/xcal/output.scm b/module/vcomponent/formats/xcal/output.scm
index 87ebd32b..e4a84efb 100644
--- a/module/vcomponent/formats/xcal/output.scm
+++ b/module/vcomponent/formats/xcal/output.scm
@@ -8,6 +8,9 @@
:use-module (datetime)
:use-module (srfi srfi-1)
:use-module (calp translation)
+ :use-module (calp namespaces)
+ :use-module (sxml namespaced)
+ :use-module (sxml namespaced util)
:export (vcomponent->sxcal ns-wrap))
@@ -56,7 +59,7 @@
[(memv key '(GEO))
(lambda (_ v)
- `(geo
+ `(,(xml xcal 'geo)
(latitude ,(geo-latitude v))
(longitude ,(geo-longitude v))))]
@@ -70,19 +73,20 @@
(get-writer 'TEXT)]
[else
- (warning (_ "Unknown key ~a") key)
+ (warning (G_ "Unknown key ~a") key)
(get-writer 'TEXT)]))
- (writer ((@@ (vcomponent base) get-vline-parameters) vline) (value vline)))
+ (writer ((@@ (vcomponent base) get-vline-parameters) vline)
+ (value vline)))
(define (property->value-tag tag . values)
(if (or (eq? tag 'VALUE)
(internal-field? tag))
#f
- `(,(downcase-symbol tag)
+ `(,(xml xcal (downcase-symbol tag))
,@(map (lambda (v)
;; TODO parameter types!!!! (rfc6321 3.5.)
- `(text ,(->string v)))
+ `(,(xml xcal 'text) ,(->string v)))
values))))
;; ((key value ...) ...) -> `(parameters , ... )
@@ -92,15 +96,14 @@
parameters))
(unless (null? outparams)
- `(parameters ,@outparams)))
+ `(,(xml xcal 'parameters) ,@outparams)))
(define (vcomponent->sxcal component)
(define tagsymb (downcase-symbol (type component)))
-
(remove null?
- `(,tagsymb
+ `(,(xml xcal tagsymb)
;; only have <properties> when it's non-empty.
,(let ((props
(filter-map
@@ -109,7 +112,7 @@
[(key vlines ...)
(remove null?
- `(,(downcase-symbol key)
+ `(,(xml xcal (downcase-symbol key))
,(parameters-tag (reduce assq-merge
'() (map parameters vlines)))
,@(for vline in vlines
@@ -117,18 +120,22 @@
[(key . vline)
(remove null?
- `(,(downcase-symbol key)
+ `(,(xml xcal (downcase-symbol key))
,(parameters-tag (parameters vline))
,(vline->value-tag vline)))])
- (properties component))))
+ ;; NOTE this sort is unnecesasary, but here so tests can work
+ ;; Possibly add it as a flag instead
+ (sort* (properties component)
+ string< (compose symbol->string car)))))
(unless (null? props)
- `(properties
+ `(,(xml xcal 'properties)
;; NOTE
;; (x-hnh-calendar-name (text ,(prop (parent component) 'NAME)))
,@props)))
,(unless (null? (children component))
- `(components ,@(map vcomponent->sxcal (children component)))))))
+ `(,(xml xcal 'components)
+ ,@(map vcomponent->sxcal (children component)))))))
(define (ns-wrap sxml)
- `(icalendar (@ (xmlns "urn:ietf:params:xml:ns:icalendar-2.0"))
- ,sxml))
+ `(,(xml xcal 'icalendar)
+ ,sxml))
diff --git a/module/vcomponent/formats/xcal/parse.scm b/module/vcomponent/formats/xcal/parse.scm
index 8537956a..7ed8c637 100644
--- a/module/vcomponent/formats/xcal/parse.scm
+++ b/module/vcomponent/formats/xcal/parse.scm
@@ -3,18 +3,23 @@
:use-module (hnh util exceptions)
:use-module (base64)
:use-module (ice-9 match)
+ :use-module (calp namespaces)
+ :use-module (sxml namespaced)
+ :use-module (sxml namespaced util)
:use-module (sxml match)
:use-module (vcomponent)
:use-module (vcomponent geo)
:use-module (vcomponent formats common types)
:use-module (datetime)
:use-module (srfi srfi-1)
+ :use-module (srfi srfi-71)
+ :use-module (srfi srfi-88)
:use-module (calp translation)
:export (sxcal->vcomponent)
)
;; symbol, ht, (list a) -> non-list
-(define (handle-value type props value)
+(define (handle-value type parameters value)
(case type
[(binary)
@@ -25,17 +30,17 @@
[(boolean) (string=? "true" (car value))]
;; TODO possibly trim whitespace on text fields
- [(cal-address uri text unknown) (car value)]
+ [(cal-address uri text unknown) (string-concatenate value)]
[(date)
;; TODO this is correct, but ensure remaining types
- (hashq-set! props 'VALUE "DATE")
+ (hashq-set! parameters 'VALUE "DATE")
(parse-iso-date (car value))]
[(date-time) (parse-iso-datetime (car value))]
[(duration)
- ((get-parser 'DURATION) props value)]
+ ((get-parser 'DURATION) parameters value)]
[(float integer) ; (3.0)
(string->number (car value))]
@@ -84,7 +89,7 @@
bymonth bysetpos)
(string->number value))
(else (scm-error 'key-error "handle-value"
- (_ "Invalid type ~a, with value ~a")
+ (G_ "Invalid type ~a, with value ~a")
(list type value)
#f))))))
@@ -96,35 +101,39 @@
(for key in '(bysecond byminute byhour byday bymonthday
byyearday byweekno bymonth bysetpos
freq until count interval wkst)
- (define values (assoc-ref-all value key))
- (if (null? values)
- #f
- (case key
- ;; These fields all have zero or one value
- ((freq until count interval wkst)
- (list (symbol->keyword key)
- (parse-value-of-that-type
- key (car (map car values)))))
- ;; these fields take lists
- ((bysecond byminute byhour byday bymonthday
- byyearday byweekno bymonth bysetpos)
- (list (symbol->keyword key)
- (map (lambda (v) (parse-value-of-that-type key v))
- (map car values))))
- (else (scm-error 'misc-error "handle-value"
- "Invalid key ~s"
- (list key)
- #f)))))))))]
+ (cond ((find-element (xml xcal key) value)
+ => (lambda (v)
+ (case key
+ ;; These fields all have zero or one value
+ ((freq until count interval wkst)
+ (list (symbol->keyword key)
+ (parse-value-of-that-type
+ key (cadr v))))
+ ;; these fields take lists
+ ((bysecond byminute byhour byday bymonthday
+ byyearday byweekno bymonth bysetpos)
+ (list (symbol->keyword key)
+ (map (lambda (v) (parse-value-of-that-type key v))
+ (cadr v))))
+ (else (scm-error 'misc-error "handle-value"
+ "Invalid key ~s"
+ (list key)
+ #f)))))
+ (else #f)))))))]
[(time) (parse-iso-time (car value))]
- [(utc-offset) ((get-parser 'UTC-OFFSET) props (car value))]
+ [(utc-offset) ((get-parser 'UTC-OFFSET) parameters (car value))]
[(geo) ; ((long 1) (lat 2))
(sxml-match
(cons 'geo value)
[(geo (latitude ,x) (longitude ,y))
- ((@ (vcomponent geo) make-geo) x y)])]))
+ ((@ (vcomponent geo) make-geo) x y)])]
+
+ [else (scm-error 'misc-error "handle-value"
+ "Unknown value type: ~s"
+ (list type) #f)]))
(define (symbol-upcase symb)
(-> symb
@@ -134,15 +143,20 @@
(define (handle-parameters parameters)
+ ;; (assert (element-matches? (xml xcal 'parameters)
+ ;; parameters))
+
(define ht (make-hash-table))
- (for param in parameters
- (match param
- [(ptag (ptype pvalue ...) ...)
- ;; TODO parameter type (rfc6321 3.5.)
- ;; TODO multi-valued parameters!!!
- (hashq-set! ht (symbol-upcase ptag)
- (car (concatenate pvalue)))]))
+ (for param in (cdr parameters)
+ (define ptag (xml-element-tagname (car param)))
+ ;; (define-values (ptype pvalue) (car+cdr cdr))
+ ;; TODO multi-valued parameters!!!
+ (define-values (pytpe pvalue) (car+cdr (cadr param)))
+ ;; TODO parameter type (rfc6321 3.5.)
+ ;; TODO namespaces
+ (hashq-set! ht (symbol-upcase ptag)
+ (concatenate pvalue)))
ht)
(define* (parse-enum str enum optional: (allow-other #t))
@@ -153,11 +167,12 @@
;; symbol non-list -> non-list
-(define (handle-tag tag-name data)
+(define (handle-tag xml-tag data)
+ (define tag-name (xml-element-tagname xml-tag))
(case tag-name
[(request-status)
;; TODO
- (warning (_ "Request status not yet implemented"))
+ (warning (G_ "Request status not yet implemented"))
#f]
((transp) (parse-enum
@@ -174,6 +189,49 @@
data '(AUDIO DISPLAY EMAIL NONE)))
[else data]))
+(define (handle-single-property! component tree)
+ (define xml-tag (car tree))
+ (define tag (xml-element-tagname xml-tag))
+ (define tag* (symbol-upcase tag))
+
+ (define body (cdr tree))
+
+ ;; TODO request-status
+ (define-values (parameters data)
+ (if (element-matches? (xml xcal 'parameters)
+ (car body))
+ (values (handle-parameters (car body))
+ (cdr body))
+ (values (make-hash-table)
+ body)))
+
+ (for typetag in data
+ (define type (xml-element-tagname (car typetag)))
+ ;; TODO multi valued data
+ (define raw-value (cdr typetag))
+ (define vline
+ (make-vline tag* (handle-tag
+ xml-tag
+ (let ((v (handle-value type parameters raw-value)))
+ ;; TODO possibly more list fields
+ ;; (if (eq? tag 'categories)
+ ;; (string-split v #\,)
+ ;; v)
+
+ v))
+ parameters))
+ (if (memv tag* '(ATTACH ATTENDEE CATEGORIES
+ COMMENT CONTACT EXDATE
+ REQUEST-STATUS RELATED-TO
+ RESOURCES RDATE
+ ;; x-prop
+ ;; iana-prop
+ ))
+ (aif (prop* component tag*)
+ (set! (prop* component tag*) (cons vline it))
+ (set! (prop* component tag*) (list vline)))
+ (set! (prop* component tag*) vline))))
+
;; Note
;; This doesn't verify the inter-field validity of the object,
;; meaning that value(DTSTART) == DATE and value(DTEND) == DATE-TIME
@@ -181,83 +239,29 @@
;; TODO
;; since we are feeding user input into this it really should be fixed.
(define (sxcal->vcomponent sxcal)
- (define type (symbol-upcase (car sxcal)))
+
+ ;; TODO the surrounding icalendar element needs to be removed BEFORE this procedue is called
+
+ (define xml-tag (car sxcal))
+ (define type (symbol-upcase (xml-element-tagname xml-tag)))
(define component (make-vcomponent type))
- (awhen (assoc-ref sxcal 'properties)
+ (awhen (find-element (xml xcal 'properties) (cdr sxcal))
;; Loop over multi valued fields, creating one vline
;; for every value. So
;; KEY;p=1:a,b
;; would be expanded into
;; KEY;p=1:a
;; KEY;p=1:b
- (for property in it
- (match property
- ;; TODO request-status
-
- [(tag ('parameters parameters ...)
- (type value ...) ...)
- (let ((params (handle-parameters parameters))
- (tag* (symbol-upcase tag)))
- (for (type value) in (zip type value)
- ;; ignore empty fields
- ;; mostly for <text/>
- (unless (null? value)
- (let ()
- (define vline
- (make-vline tag*
- (handle-tag
- tag (handle-value type params value))
- params))
- (if (memv tag* '(ATTACH ATTENDEE CATEGORIES
- COMMENT CONTACT EXDATE
- REQUEST-STATUS RELATED-TO
- RESOURCES RDATE
- ;; x-prop
- ;; iana-prop
- ))
- (aif (prop* component tag*)
- (set! (prop* component tag*) (cons vline it))
- (set! (prop* component tag*) (list vline)))
- ;; else
- (set! (prop* component tag*) vline))
- ))))]
-
- [(tag (type value ...) ...)
- (for (type value) in (zip type value)
- ;; ignore empty fields
- ;; mostly for <text/>
- (unless (null? value)
- (let ((params (make-hash-table))
- (tag* (symbol-upcase tag)))
- (define vline
- (make-vline tag*
- (handle-tag
- tag (let ((v (handle-value type params value)))
- ;; TODO possibly more list fields
- (if (eq? tag 'categories)
- (string-split v #\,)
- v)))
- params))
- ;;
-
- (if (memv tag* '(ATTACH ATTENDEE CATEGORIES
- COMMENT CONTACT EXDATE
- REQUEST-STATUS RELATED-TO
- RESOURCES RDATE
- ;; x-prop
- ;; iana-prop
- ))
- (aif (prop* component tag*)
- (set! (prop* component tag*) (cons vline it))
- (set! (prop* component tag*) (list vline)))
- ;; else
- (set! (prop* component tag*) vline))
- )))])))
+ (map (lambda (x) (handle-single-property! component x))
+ (cdr it)))
;; children
- (awhen (assoc-ref sxcal 'components)
- (for child in (map sxcal->vcomponent it)
- (add-child! component child)))
+ (awhen (find-element (xml xcal 'components) (cdr sxcal))
+ ;; NOTE Order of children is insignificant, but this allows
+ ;; diffs to be stable (which is used by the format tests).
+ (for child in (map sxcal->vcomponent
+ (reverse (cdr it)))
+ (reparent! component child)))
component)
diff --git a/module/vcomponent/formats/xcal/types.scm b/module/vcomponent/formats/xcal/types.scm
index a88b6b04..82121d5e 100644
--- a/module/vcomponent/formats/xcal/types.scm
+++ b/module/vcomponent/formats/xcal/types.scm
@@ -3,16 +3,18 @@
:use-module (vcomponent formats ical types)
:use-module (datetime)
:use-module (calp translation)
+ :use-module ((calp namespaces) :select (xcal))
+ :use-module ((sxml namespaced) :select (xml))
:export (get-writer))
(define (write-boolean _ v)
- `(boolean ,(if v "true" "false")))
+ `(,(xml xcal 'boolean) ,(if v "true" "false")))
(define (write-date _ v)
- `(date ,(date->string v "~Y-~m-~d")))
+ `(,(xml xcal 'date) ,(date->string v "~Y-~m-~d")))
(define (write-datetime p v)
- `(date-time
+ `(,(xml xcal 'date-time)
,(datetime->string
(hashq-ref p '-X-HNH-ORIGINAL v)
;; 'Z' should be included for UTC,
@@ -21,17 +23,17 @@
"~Y-~m-~dT~H:~M:~S~Z")))
(define (write-time _ v)
- `(time ,(time->string v "~H:~M:S")))
+ `(,(xml xcal 'time) ,(time->string v "~H:~M:S")))
(define (write-recur _ v)
- `(recur ,@((@@ (vcomponent recurrence internal) recur-rule->rrule-sxml) v)))
+ `(,(xml xcal 'recur) ,@((@@ (vcomponent recurrence internal) recur-rule->rrule-sxml) v)))
;; sepparate since this text shouldn't be escaped
(define (write-text _ v)
;; TODO out type should be xsd:string.
;; Look into what that means, and escape
;; from there
- `(text ,v))
+ `(,(xml xcal 'text) ,v))
@@ -40,7 +42,7 @@
#| TODO PERIOD |# URI UTC-OFFSET)
(hashq-set! sxml-writers simple-type
(lambda (p v)
- `(,(downcase-symbol simple-type)
+ `(,(xml xcal (downcase-symbol simple-type))
,(((@ (vcomponent formats ical types) get-writer) simple-type) p v)))))
(hashq-set! sxml-writers 'BOOLEAN write-boolean)
@@ -52,4 +54,4 @@
(define (get-writer type)
(or (hashq-ref sxml-writers type #f)
- (error (_ "No writer for type") type)))
+ (error (G_ "No writer for type") type)))
diff --git a/module/vcomponent/recurrence/display/en.scm b/module/vcomponent/recurrence/display/en.scm
index c711a75c..18d11dba 100644
--- a/module/vcomponent/recurrence/display/en.scm
+++ b/module/vcomponent/recurrence/display/en.scm
@@ -26,13 +26,13 @@
(list "every "
(add-enumeration-punctuation
(map (lambda (d) (list (week-day-name (cdr d))))
- (cadr group)
+ (cdr group)
)))]
[else
(list (number->string-ordinal (car group)) " "
(add-enumeration-punctuation
(map (lambda (d) (list (week-day-name (cdr d)) "en"))
- (cadr group))))])
+ (cdr group))))])
)
groups))))
diff --git a/module/vcomponent/recurrence/display/sv.scm b/module/vcomponent/recurrence/display/sv.scm
index 2bd70657..ee8fc3fd 100644
--- a/module/vcomponent/recurrence/display/sv.scm
+++ b/module/vcomponent/recurrence/display/sv.scm
@@ -31,7 +31,7 @@
(list "varje "
(add-enumeration-punctuation
(map (lambda (d) (list (week-day-name (cdr d))))
- (cadr group)
+ (cdr group)
)))]
[else
(list (number->string-ordinal
@@ -40,7 +40,7 @@
" "
(add-enumeration-punctuation
(map (lambda (d) (list (week-day-name (cdr d)) "en"))
- (cadr group))))])
+ (cdr group))))])
)
groups))))
diff --git a/module/vcomponent/recurrence/internal.scm b/module/vcomponent/recurrence/internal.scm
index 94c4cccf..9bf425ac 100644
--- a/module/vcomponent/recurrence/internal.scm
+++ b/module/vcomponent/recurrence/internal.scm
@@ -79,9 +79,18 @@
;; to prevent creation of invalid rules.
;; This was made apparent when wkst was (incorrectly) set to MO,
;; which later crashed generate-recurrence-set.
- (make-recur-rule% freq until count interval bysecond byminute byhour
- byday bymonthday byyearday byweekno bymonth bysetpos
- wkst))
+
+ ;; Allow `(cons #f day)' to be written as just `day'.
+ (let ((byday* (if byday
+ (map (lambda (day)
+ (if (number? day)
+ (cons #f day)
+ day))
+ byday)
+ #f)))
+ (make-recur-rule% freq until count interval bysecond byminute byhour
+ byday* bymonthday byyearday byweekno bymonth bysetpos
+ wkst)))
;; only print fields with actual values.
(set-record-type-printer!
diff --git a/module/vcomponent/util/instance.scm b/module/vcomponent/util/instance.scm
index a18085eb..2310c5bc 100644
--- a/module/vcomponent/util/instance.scm
+++ b/module/vcomponent/util/instance.scm
@@ -1,4 +1,5 @@
(define-module (vcomponent util instance)
+ :use-module (srfi srfi-88)
:use-module (hnh util)
:use-module (calp translation)
:use-module ((vcomponent util instance methods) :select (make-instance))
@@ -14,6 +15,6 @@
(define-once global-event-object
(make-instance ((@ (vcomponent config) calendar-files))))
-(define (reload)
- (begin (set! global-event-object (make-instance ((@ (vcomponent config) calendar-files))))
- (format (current-error-port) (_ "Reload done~%"))))
+(define* (reload optional: (files ((@ (vcomponent config) calendar-files))))
+ (begin (set! global-event-object (make-instance files))
+ (format (current-error-port) (G_ "Reload done~%"))))
diff --git a/module/vcomponent/util/instance/methods.scm b/module/vcomponent/util/instance/methods.scm
index 193a0304..fef83958 100644
--- a/module/vcomponent/util/instance/methods.scm
+++ b/module/vcomponent/util/instance/methods.scm
@@ -80,7 +80,7 @@
(define-method (initialize (this <events>) args)
(next-method)
- (format (current-error-port) (_ "Building <events> from~%"))
+ (format (current-error-port) (G_ "Building <events> from~%"))
(for calendar in (slot-ref this 'calendar-files)
(format (current-error-port) " - ~a~%" calendar))
@@ -96,7 +96,7 @@
type (concatenate
(map children (slot-ref this 'calendars)))))
(events (awhen (assoc-ref groups 'VEVENT)
- (car it)))
+ it))
(removed remaining (partition (extract 'X-HNH-REMOVED) events)))
;; TODO figure out what to do with removed events
@@ -125,7 +125,7 @@
;;; with the same UID, which is BAD.
(define-method (add-event (this <events>) calendar event)
- (add-child! calendar event)
+ (reparent! calendar event)
(unless (prop event 'UID)
(set! (prop event 'UID) (uuid)))
@@ -174,13 +174,17 @@
(define-method (add-and-save-event (this <events>) calendar event)
+
+ ((@ (vcomponent validate) validate-event) event)
+
(cond
[(get-event-by-uid this (prop event 'UID))
=> (lambda (old-event)
+ (define old-calendar (parent old-event))
;; remove old instance of event from runtime
(remove-event this old-event)
- (remove-child! (parent old-event) old-event)
+ (abandon! old-calendar old-event)
;; Add new event to runtime,
;; MUST be done after since the two events SHOULD share UID.
@@ -196,13 +200,13 @@
;; save-event sets -X-HNH-FILENAME from the UID. This is fine
;; since the two events are guaranteed to have the same UID.
(unless ((@ (vcomponent formats vdir save-delete) save-event) event)
- (throw 'misc-error (_ "Saving event to disk failed.")))
-
+ (throw 'misc-error (G_ "Saving event to disk failed.")))
- (unless (eq? calendar (parent old-event))
+ #;
+ (unless (eq? calendar old-calendar)
;; change to a new calendar
(format (current-error-port)
- (_ "Unlinking old event from ~a~%")
+ (G_ "Unlinking old event from ~a~%")
(prop old-event '-X-HNH-FILENAME))
;; NOTE that this may fail, leading to a duplicate event being
;; created (since we save beforehand). This is just a minor problem
@@ -212,7 +216,9 @@
(format (current-error-port)
- (_ "Event updated ~a~%") (prop event 'UID)))]
+ (G_ "Event ~a updated in ~a~%")
+ (prop event 'UID)
+ (prop calendar 'NAME)))]
[else
(add-event this calendar event)
@@ -222,7 +228,9 @@
;; NOTE Posibly defer save to a later point.
;; That would allow better asyncronous preformance.
(unless ((@ (vcomponent formats vdir save-delete) save-event) event)
- (throw 'misc-error (_ "Saving event to disk failed.")))
+ (throw 'misc-error (G_ "Saving event to disk failed.")))
(format (current-error-port)
- (_ "Event inserted ~a~%") (prop event 'UID))]))
+ (G_ "Event ~a added to ~a~%")
+ (prop event 'UID)
+ (prop calendar 'NAME))]))
diff --git a/module/vcomponent/util/parse-cal-path.scm b/module/vcomponent/util/parse-cal-path.scm
index cf03db88..fe3a6b7d 100644
--- a/module/vcomponent/util/parse-cal-path.scm
+++ b/module/vcomponent/util/parse-cal-path.scm
@@ -1,3 +1,5 @@
+;;; TODO remove this module, it should be part of the vdir interface
+
(define-module (vcomponent util parse-cal-path)
:use-module (hnh util)
:use-module ((calp util time) :select (report-time!))
@@ -21,14 +23,14 @@
(set! (prop comp '-X-HNH-SOURCETYPE) 'file)
comp) ]
[(directory)
- (report-time! (_ "Parsing ~a") path)
+ (report-time! (G_ "Parsing ~a") path)
(let ((comp (parse-vdir path)))
(set! (prop comp '-X-HNH-SOURCETYPE) 'vdir
(prop comp '-X-HNH-DIRECTORY) path)
comp)]
[(block-special char-special fifo socket unknown symlink)
=> (lambda (t) (scm-error 'misc-error "parse-cal-path"
- (_ "Can't parse file of type ~s")
+ (G_ "Can't parse file of type ~s")
(list t)
#f))]))
diff --git a/module/vcomponent/validate.scm b/module/vcomponent/validate.scm
new file mode 100644
index 00000000..8881c95f
--- /dev/null
+++ b/module/vcomponent/validate.scm
@@ -0,0 +1,16 @@
+(define-module (vcomponent validate)
+ :use-module (vcomponent)
+ :use-module (datetime)
+ :use-module ((hnh util exceptions)
+ :select (warning))
+ :use-module (calp translation)
+ :export (validate-event))
+
+(define (validate-event component)
+ (unless (date/-time<=
+ (prop component 'DTSTART)
+ (prop component 'DTEND))
+ (warning (G_ "end (~a) must be equal to or greater than start (~a)")
+ (prop component 'DTEND)
+ (prop component 'DTSTART)))
+ )