summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ical.c301
-rw-r--r--test/Makefile.am1
-rw-r--r--test/data/ical-014.ical106
-rwxr-xr-xtest/ical-014.sh28
4 files changed, 354 insertions, 82 deletions
diff --git a/src/ical.c b/src/ical.c
index 806ba30..1f3b413 100644
--- a/src/ical.c
+++ b/src/ical.c
@@ -36,11 +36,14 @@
#include <strings.h>
#include <sys/types.h>
+#include <ctype.h>
#include "calcurse.h"
#define ICALDATEFMT "%Y%m%d"
#define ICALDATETIMEFMT "%Y%m%dT%H%M%S"
+#define SEPARATOR "-- \n"
+#define INDENT " "
typedef enum {
ICAL_VEVENT,
@@ -83,22 +86,29 @@ static void ical_export_footer(FILE *);
static const char *ical_recur_type[NBRECUR] =
{ "DAILY", "WEEKLY", "MONTHLY", "YEARLY" };
-/* Escape characters in field before printing */
-static void ical_format_line(FILE * stream, char * field, char * msg)
+static const char *ical_wday[] =
+ {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
+
+/*
+ * Encode a string as a property value of type TEXT (RFC 5545, 3.3.11).
+ */
+static void ical_format_line(FILE *stream, char *property, char *msg)
{
char * p;
- fputs(field, stream);
+ fputs(property, stream);
for (p = msg; *p; p++) {
switch (*p) {
+ case '\n':
+ fprintf(stream, "\\%c", 'n');
+ break;
case ',':
case ';':
case '\\':
fprintf(stream, "\\%c", *p);
break;
default:
- fputc(*p, stream);
- break;
+ fputc(*p, stream);
}
}
fputc('\n', stream);
@@ -115,12 +125,154 @@ static void ical_export_valarm(FILE * stream)
fputs("END:VALARM\n", stream);
}
+static void ical_export_rrule(FILE *stream, struct rpt *rpt, ical_vevent_e item,
+ char *buf)
+{
+ llist_item_t *j;
+ int d;
+ char *fmt = item == EVENT ? ICALDATEFMT :
+ item == APPOINTMENT ? ICALDATETIMEFMT :
+ NULL;
+
+ fprintf(stream, "RRULE:FREQ=%s", ical_recur_type[rpt->type]);
+ if (rpt->freq > 1)
+ fprintf(stream, ";INTERVAL=%d", rpt->freq);
+ if (rpt->until) {
+ date_sec2date_fmt(rpt->until, fmt, buf);
+ fprintf(stream, ";UNTIL=%s", buf);
+ }
+ if (LLIST_FIRST(&rpt->bymonth)) {
+ fputs(";BYMONTH=", stream);
+ LLIST_FOREACH(&rpt->bymonth, j) {
+ d = *(int *)LLIST_GET_DATA(j);
+ fprintf(stream, "%d", d);
+ if (LLIST_NEXT(j))
+ fputc(',', stream);
+ }
+ }
+ if (LLIST_FIRST(&rpt->bywday)) {
+ int ord;
+ char sign;
+
+ fputs(";BYDAY=", stream);
+ LLIST_FOREACH(&rpt->bywday, j) {
+ d = *(int *)LLIST_GET_DATA(j);
+ sign = d < 0 ? '-' : '+';
+ d = abs(d);
+ ord = d / 7;
+ d = d % 7;
+ if (ord == 0)
+ fprintf(stream, "%s", ical_wday[d]);
+ else
+ fprintf(stream, "%c%d%s", sign, ord, ical_wday[d]);
+ if (LLIST_NEXT(j))
+ fputc(',', stream);
+ }
+ }
+ if (LLIST_FIRST(&rpt->bymonthday)) {
+ fputs(";BYMONTHDAY=", stream);
+ LLIST_FOREACH(&rpt->bymonthday, j) {
+ d = *(int *)LLIST_GET_DATA(j);
+ fprintf(stream, "%d", d);
+ if (LLIST_NEXT(j))
+ fputc(',', stream);
+ }
+ }
+ fputc('\n', stream);
+}
+
+/*
+ * Copy the characters (lines) between "a" and "z" into an allocated string,
+ * un-indent it and return it. Note that the final character, a newline, is
+ * overwritten with '\0'.
+ */
+static char *ical_unindent(char *a, char *z)
+{
+ char *p, *q; int len;
+
+ len = z - a + 1;
+
+ p = mem_malloc(len);
+ strncpy(p, a, len);
+ p[len - 1] = '\0';
+ while ((q = strstr(p, "\n" INDENT))) {
+ while (*(q + 1 + strlen(INDENT))) {
+ *(q + 1) = *(q + 1 + strlen(INDENT));
+ q++;
+ }
+ *(q + 1) = '\0';
+ }
+ return p;
+}
+
+static void ical_export_note(FILE *stream, char *name)
+{
+ char *note_file, *p, *q, *r, *rest;
+ char *property[] = {
+ "Location: ",
+ "Comment: ",
+ NULL
+ };
+ char *PROPERTY[] = {
+ "LOCATION:",
+ "COMMENT:"
+ };
+ struct string note;
+ char lbuf[BUFSIZ];
+ FILE *fp;
+ int has_desc, has_prop, i;
+
+ asprintf(&note_file, "%s/%s", path_notes, name);
+ if (!(fp = fopen(note_file, "r")))
+ return;
+ string_init(&note);
+ while (fgets(lbuf, BUFSIZ, fp))
+ string_catf(&note, "%s", lbuf);
+ fclose(fp);
+
+ has_desc = has_prop = 0;
+ rest = note.buf;
+ if ((p = strstr(note.buf, SEPARATOR))) {
+ has_prop = 1;
+ rest = p + strlen(SEPARATOR);
+ if (p != note.buf) {
+ has_desc = 1;
+ *(--p) = '\0';
+ }
+ } else {
+ has_desc = 1;
+ note.buf[strlen(note.buf) - 1] = '\0';
+ }
+
+ if (has_desc)
+ ical_format_line(stream, "DESCRIPTION:", note.buf);
+
+ if (!has_prop)
+ goto cleanup;
+ for (i = 0; property[i]; i++) {
+ if ((p = strstr(rest, property[i])))
+ p += strlen(property[i]);
+ else
+ continue;
+ /* Find end of property. */
+ for (q = p;
+ (q = strchr(q, '\n')) && starts_with(q + 1, INDENT);
+ q++) ;
+ /* Extract property line(s). */
+ r = ical_unindent(p, q);
+ ical_format_line(stream, PROPERTY[i], r);
+ mem_free(r);
+ }
+cleanup:
+ mem_free(note.buf);
+}
+
/* Export header. */
static void ical_export_header(FILE * stream)
{
fputs("BEGIN:VCALENDAR\n", stream);
- fprintf(stream, "PRODID:-//calcurse//NONSGML v%s//EN\n", VERSION);
fputs("VERSION:2.0\n", stream);
+ fprintf(stream, "PRODID:-//calcurse//NONSGML v%s//EN\n", VERSION);
}
/* Export footer. */
@@ -133,26 +285,21 @@ static void ical_export_footer(FILE * stream)
static void ical_export_recur_events(FILE * stream, int export_uid)
{
llist_item_t *i, *j;
- char ical_date[BUFSIZ];
+ char ical_date[BUFSIZ], *hash;
LLIST_FOREACH(&recur_elist, i) {
struct recur_event *rev = LLIST_GET_DATA(i);
- date_sec2date_fmt(rev->day, ICALDATEFMT, ical_date);
fputs("BEGIN:VEVENT\n", stream);
- fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date);
- fprintf(stream, "RRULE:FREQ=%s;INTERVAL=%d",
- ical_recur_type[rev->rpt->type], rev->rpt->freq);
-
- if (rev->rpt->until != 0) {
- date_sec2date_fmt(rev->rpt->until, ICALDATEFMT,
- ical_date);
- fprintf(stream, ";UNTIL=%s\n", ical_date);
- } else {
- fputc('\n', stream);
+ if (export_uid) {
+ hash = recur_event_hash(rev);
+ fprintf(stream, "UID:%s\n", hash);
+ mem_free(hash);
}
-
+ date_sec2date_fmt(rev->day, ICALDATEFMT, ical_date);
+ fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date);
+ ical_export_rrule(stream, rev->rpt, EVENT, ical_date);
if (LLIST_FIRST(&rev->exc)) {
- fputs("EXDATE:", stream);
+ fputs("EXDATE;VALUE=DATE:", stream);
LLIST_FOREACH(&rev->exc, j) {
struct excp *exc = LLIST_GET_DATA(j);
date_sec2date_fmt(exc->st, ICALDATETIMEFMT,
@@ -161,15 +308,9 @@ static void ical_export_recur_events(FILE * stream, int export_uid)
fputc(LLIST_NEXT(j) ? ',' : '\n', stream);
}
}
-
ical_format_line(stream, "SUMMARY:", rev->mesg);
-
- if (export_uid) {
- char *hash = recur_event_hash(rev);
- fprintf(stream, "UID:%s\n", hash);
- mem_free(hash);
- }
-
+ if (rev->note)
+ ical_export_note(stream, rev->note);
fputs("END:VEVENT\n", stream);
}
}
@@ -178,21 +319,21 @@ static void ical_export_recur_events(FILE * stream, int export_uid)
static void ical_export_events(FILE * stream, int export_uid)
{
llist_item_t *i;
- char ical_date[BUFSIZ];
+ char ical_date[BUFSIZ], *hash;
LLIST_FOREACH(&eventlist, i) {
struct event *ev = LLIST_TS_GET_DATA(i);
- date_sec2date_fmt(ev->day, ICALDATEFMT, ical_date);
fputs("BEGIN:VEVENT\n", stream);
- fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date);
- ical_format_line(stream, "SUMMARY:", ev->mesg);
-
if (export_uid) {
- char *hash = event_hash(ev);
+ hash = event_hash(ev);
fprintf(stream, "UID:%s\n", hash);
mem_free(hash);
}
-
+ date_sec2date_fmt(ev->day, ICALDATEFMT, ical_date);
+ fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date);
+ ical_format_line(stream, "SUMMARY:", ev->mesg);
+ if (ev->note)
+ ical_export_note(stream, ev->note);
fputs("END:VEVENT\n", stream);
}
}
@@ -201,16 +342,30 @@ static void ical_export_events(FILE * stream, int export_uid)
static void ical_export_recur_apoints(FILE * stream, int export_uid)
{
llist_item_t *i, *j;
- char ical_datetime[BUFSIZ];
- char ical_date[BUFSIZ];
+ char ical_datetime[BUFSIZ], *hash;
+ time_t tod;
LLIST_TS_LOCK(&recur_alist_p);
LLIST_TS_FOREACH(&recur_alist_p, i) {
struct recur_apoint *rapt = LLIST_TS_GET_DATA(i);
+ /*
+ * Add time-of-day to UNTIL/EXDATE.
+ * In calcurse until/exception is a date (midnight), but in
+ * RFC 5545 UNTIL/EXDATE is a DATE-TIME value type by default.
+ */
+ tod = get_item_time(rapt->start);
+ if (rapt->rpt->until)
+ rapt->rpt->until += tod;
+
date_sec2date_fmt(rapt->start, ICALDATETIMEFMT,
ical_datetime);
fputs("BEGIN:VEVENT\n", stream);
+ if (export_uid) {
+ hash = recur_apoint_hash(rapt);
+ fprintf(stream, "UID:%s\n", hash);
+ mem_free(hash);
+ }
fprintf(stream, "DTSTART:%s\n", ical_datetime);
if (rapt->dur > 0) {
fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n",
@@ -219,38 +374,22 @@ static void ical_export_recur_apoints(FILE * stream, int export_uid)
(rapt->dur / MININSEC) % HOURINMIN,
rapt->dur % MININSEC);
}
- fprintf(stream, "RRULE:FREQ=%s;INTERVAL=%d",
- ical_recur_type[rapt->rpt->type], rapt->rpt->freq);
-
- if (rapt->rpt->until != 0) {
- date_sec2date_fmt(rapt->rpt->until + HOURINSEC,
- ICALDATEFMT, ical_date);
- fprintf(stream, ";UNTIL=%s\n", ical_date);
- } else {
- fputc('\n', stream);
- }
-
+ ical_export_rrule(stream, rapt->rpt, APPOINTMENT, ical_datetime);
if (LLIST_FIRST(&rapt->exc)) {
fputs("EXDATE:", stream);
LLIST_FOREACH(&rapt->exc, j) {
struct excp *exc = LLIST_GET_DATA(j);
- date_sec2date_fmt(exc->st, ICALDATETIMEFMT,
- ical_date);
- fprintf(stream, "%s", ical_date);
+ date_sec2date_fmt(exc->st + tod, ICALDATETIMEFMT,
+ ical_datetime);
+ fprintf(stream, "%s", ical_datetime);
fputc(LLIST_NEXT(j) ? ',' : '\n', stream);
}
}
-
ical_format_line(stream, "SUMMARY:", rapt->mesg);
+ if (rapt->note)
+ ical_export_note(stream, rapt->note);
if (rapt->state & APOINT_NOTIFY)
ical_export_valarm(stream);
-
- if (export_uid) {
- char *hash = recur_apoint_hash(rapt);
- fprintf(stream, "UID:%s\n", hash);
- mem_free(hash);
- }
-
fputs("END:VEVENT\n", stream);
}
LLIST_TS_UNLOCK(&recur_alist_p);
@@ -260,14 +399,19 @@ static void ical_export_recur_apoints(FILE * stream, int export_uid)
static void ical_export_apoints(FILE * stream, int export_uid)
{
llist_item_t *i;
- char ical_datetime[BUFSIZ];
+ char ical_datetime[BUFSIZ], *hash;
LLIST_TS_LOCK(&alist_p);
LLIST_TS_FOREACH(&alist_p, i) {
struct apoint *apt = LLIST_TS_GET_DATA(i);
+ fputs("BEGIN:VEVENT\n", stream);
+ if (export_uid) {
+ hash = apoint_hash(apt);
+ fprintf(stream, "UID:%s\n", hash);
+ mem_free(hash);
+ }
date_sec2date_fmt(apt->start, ICALDATETIMEFMT,
ical_datetime);
- fputs("BEGIN:VEVENT\n", stream);
fprintf(stream, "DTSTART:%s\n", ical_datetime);
if (apt->dur > 0) {
fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n",
@@ -277,15 +421,10 @@ static void ical_export_apoints(FILE * stream, int export_uid)
apt->dur % MININSEC);
}
ical_format_line(stream, "SUMMARY:", apt->mesg);
+ if (apt->note)
+ ical_export_note(stream, apt->note);
if (apt->state & APOINT_NOTIFY)
ical_export_valarm(stream);
-
- if (export_uid) {
- char *hash = apoint_hash(apt);
- fprintf(stream, "UID:%s\n", hash);
- mem_free(hash);
- }
-
fputs("END:VEVENT\n", stream);
}
LLIST_TS_UNLOCK(&alist_p);
@@ -295,22 +434,23 @@ static void ical_export_apoints(FILE * stream, int export_uid)
static void ical_export_todo(FILE * stream, int export_uid)
{
llist_item_t *i;
+ char *hash;
LLIST_FOREACH(&todolist, i) {
struct todo *todo = LLIST_TS_GET_DATA(i);
fputs("BEGIN:VTODO\n", stream);
- if (todo->completed)
- fprintf(stream, "STATUS:COMPLETED\n");
- fprintf(stream, "PRIORITY:%d\n", todo->id);
- ical_format_line(stream, "SUMMARY:", todo->mesg);
-
if (export_uid) {
- char *hash = todo_hash(todo);
+ hash = todo_hash(todo);
fprintf(stream, "UID:%s\n", hash);
mem_free(hash);
}
-
+ fprintf(stream, "PRIORITY:%d\n", todo->id);
+ ical_format_line(stream, "SUMMARY:", todo->mesg);
+ if (todo->note)
+ ical_export_note(stream, todo->note);
+ if (todo->completed)
+ fprintf(stream, "STATUS:COMPLETED\n");
fputs("END:VTODO\n", stream);
}
}
@@ -507,7 +647,6 @@ static char *ical_unformat_line(char *line, int eol, int indentation)
{
struct string s;
char *p;
- const char *INDENT = " ";
string_init(&s);
for (p = line; *p; p++) {
@@ -1180,7 +1319,7 @@ static char *ical_read_note(char *line, ical_property_e property, unsigned *nosk
FILE * log)
{
const int EOL = 1,
- INDENT = (property != DESCRIPTION);
+ IND = (property != DESCRIPTION);
char *p, *pname, *notestr;
switch (property) {
@@ -1207,7 +1346,7 @@ static char *ical_read_note(char *line, ical_property_e property, unsigned *nosk
goto leave;
}
- notestr = ical_unformat_line(p, EOL, INDENT);
+ notestr = ical_unformat_line(p, EOL, IND);
if (!notestr) {
asprintf(&p, _("malformed %s."), pname);
ical_log(log, item_type, itemline, p);
@@ -1223,7 +1362,7 @@ static char *ical_read_summary(char *line, unsigned *noskipped,
ical_types_e item_type, const int itemline,
FILE * log)
{
- const int EOL = 0, INDENT = 0;
+ const int EOL = 0, IND = 0;
char *p, *summary = NULL;
p = ical_get_value(line);
@@ -1233,7 +1372,7 @@ static char *ical_read_summary(char *line, unsigned *noskipped,
goto leave;
}
- summary = ical_unformat_line(p, EOL, INDENT);
+ summary = ical_unformat_line(p, EOL, IND);
if (!summary) {
ical_log(log, item_type, itemline, _("malformed summary."));
(*noskipped)++;
@@ -1261,7 +1400,6 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
ical_vevent_e vevent_type;
ical_property_e property;
char *p, *note = NULL, *tmp, *tzid;
- const char *SEPARATOR = "-- \n";
struct string s;
struct {
llist_t exc;
@@ -1591,7 +1729,6 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
const int ITEMLINE = *lineno - !feof(fdi);
ical_property_e property;
char *note = NULL, *tmp;
- const char *SEPARATOR = "-- \n";
struct string s;
struct {
char *mesg, *desc, *loc, *comm, *note;
diff --git a/test/Makefile.am b/test/Makefile.am
index dfb97c3..daa6e77 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -61,6 +61,7 @@ TESTS = \
ical-011.sh \
ical-012.sh \
ical-013.sh \
+ ical-014.sh \
next-001.sh \
next-002.sh \
next-003.sh \
diff --git a/test/data/ical-014.ical b/test/data/ical-014.ical
new file mode 100644
index 0000000..19076bc
--- /dev/null
+++ b/test/data/ical-014.ical
@@ -0,0 +1,106 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+
+BEGIN:VEVENT
+DTSTART:20200404T204500
+DURATION:PT1H30M0S
+SUMMARY:event with one-line description
+DESCRIPTION:event with one-line description
+END:VEVENT
+
+BEGIN:VEVENT
+DTSTART:20200404T204500
+DURATION:PT1H30M0S
+SUMMARY:description and location
+DESCRIPTION:event with description\nand location
+LOCATION:Right here
+END:VEVENT
+
+BEGIN:VEVENT
+DTSTART:20200404T084100
+DURATION:PT1H30M0S
+SUMMARY:no description\, but comment
+COMMENT:Event without description: a comment\nstreching over\nthree lines
+END:VEVENT
+
+BEGIN:VEVENT
+DTSTART:20200404T084100
+DURATION:PT1H30M0S
+SUMMARY:Empty description
+DESCRIPTION:
+END:VEVENT
+
+BEGIN:VEVENT
+DTSTART:20200404T084100
+DURATION:PT1H30M0S
+SUMMARY:Empty description\, but comment
+DESCRIPTION:
+COMMENT:event with empty description
+END:VEVENT
+
+BEGIN:VEVENT
+DTSTART:20200404T204500
+DURATION:PT1H30M0S
+SUMMARY:description\, comment and location
+DESCRIPTION:event with\ndescription\ncomment\nand location
+LOCATION:Right here
+COMMENT:just a repetition of description:\nevent with\ndescription\ncomment\nand location
+END:VEVENT
+
+BEGIN:VTODO
+PRIORITY:2
+SUMMARY:todo with one-line description
+DESCRIPTION:todo with one-line description
+END:VTODO
+
+BEGIN:VTODO
+PRIORITY:3
+SUMMARY:description and location
+DESCRIPTION:todo with description\nand location
+LOCATION:Right here
+END:VTODO
+
+BEGIN:VTODO
+PRIORITY:4
+SUMMARY:no description\, but comment
+COMMENT:Todo without description. A comment\nstreching over\nthree lines
+END:VTODO
+
+BEGIN:VTODO
+PRIORITY:5
+SUMMARY:Empty description
+DESCRIPTION:
+END:VTODO
+
+BEGIN:VTODO
+PRIORITY:6
+SUMMARY:Empty description
+DESCRIPTION:
+END:VTODO
+
+BEGIN:VTODO
+SUMMARY:todo with description\, comment and location
+DESCRIPTION:todo with\ndescription\ncomment\nand location\,\nbut no priority
+LOCATION:Right here
+COMMENT:mostly a repetition of description:\ntodo with\ndescription\ncomment\nand location
+STATUS:COMPLETED
+END:VTODO
+
+BEGIN:VEVENT
+SUMMARY:Five days
+DESCRIPTION:A five-day event turned into a recurring one-day event
+COMMENT:Note file has Comment: and Import:
+DTSTART;VALUE=DATE:20200819
+DTEND;VALUE=DATE:20200824
+END:VEVENT
+
+BEGIN:VEVENT
+SUMMARY:CET
+DESCRIPTION:Date with local time and time zone reference
+LOCATION:Central Europe
+COMMENT:\nCET\n\n
+DTSTART;TZID=CET:20150223T110000
+DURATION:PT1H
+END:VEVENT
+
+END:VCALENDAR
diff --git a/test/ical-014.sh b/test/ical-014.sh
new file mode 100755
index 0000000..68f36a1
--- /dev/null
+++ b/test/ical-014.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+# Import followed by export and comparison
+
+. "${TEST_INIT:-./test-init.sh}"
+
+if [ "$1" = 'actual' ]; then
+ mkdir .calcurse || exit 1
+ cp "$DATA_DIR/conf" .calcurse || exit 1
+ "$CALCURSE" -q -D "$PWD/.calcurse" -i "$DATA_DIR/ical-014.ical"
+ "$CALCURSE" -D "$PWD/.calcurse" -x |
+ sed -n '
+ /DESCRIPTION/p
+ /LOCATION/p
+ /COMMENT/p
+ ' |
+ sort
+ rm -rf .calcurse || exit 1
+elif [ "$1" = 'expected' ]; then
+ cat "$DATA_DIR/ical-014.ical" |
+ sed -n '
+ /DESCRIPTION/p
+ /LOCATION/p
+ /COMMENT/p
+ ' |
+ sort
+else
+ ./run-test "$0"
+fi