Extend use of note file for iCal import

iCal import to an item note file is extended from DESCRIPTION to
LOCATION, COMMENT and STATUS for both events and todos.

Addresses GitHub issue #9.

Signed-off-by: Lars Henriksen <LarsHenriksen@get2net.dk>
Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
This commit is contained in:
Lars Henriksen 2020-04-07 21:29:26 +02:00 committed by Lukas Fleischer
parent 214a761564
commit e9deb6fff3
7 changed files with 479 additions and 77 deletions

View File

@ -54,6 +54,15 @@ typedef enum {
EVENT
} ical_vevent_e;
typedef enum {
NO_PROPERTY,
SUMMARY,
DESCRIPTION,
LOCATION,
COMMENT,
STATUS
} ical_property_e;
typedef struct {
enum recur_type type;
int freq;
@ -440,14 +449,16 @@ ical_store_apoint(char *mesg, char *note, long start, long dur,
}
/*
* Returns an allocated string representing the argument string with escaped
* characters decoded, or NULL on error.
* The string is assumed to be the value part of a SUMMARY or DESCRIPTION line.
* Return an allocated string containing the decoded 'line' or NULL on error.
* The last arguments are used to format a note file entry.
* The line is assumed to be the value part of a content line of type TEXT or
* INTEGER (RFC 5545, 3.3.11 and 3.3.8) without list or field separators (3.1.1).
*/
static char *ical_unformat_line(char *line)
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++) {
@ -457,6 +468,8 @@ static char *ical_unformat_line(char *line)
case 'N':
case 'n':
string_catf(&s, "%c", '\n');
if (indentation)
string_catf(&s, "%s", INDENT);
p++;
break;
case '\\':
@ -472,9 +485,7 @@ static char *ical_unformat_line(char *line)
break;
case ',':
case ';':
/*
* No list or field separator allowed.
*/
/* No list or field separator allowed. */
mem_free(s.buf);
return NULL;
default:
@ -482,6 +493,9 @@ static char *ical_unformat_line(char *line)
break;
}
}
/* Add the final EOL removed by ical_readline(). */
if (eol)
string_catf(&s, "\n");
return string_buf(&s);
}
@ -879,31 +893,54 @@ ical_read_exdate(llist_t * exc, FILE * log, char *exstr,
return 1;
}
/* Return an allocated string containing the name of the newly created note. */
static char *ical_read_note(char *line, unsigned *noskipped,
/*
* Return an allocated string containing a property value to be written in a
* note file or NULL on error.
*/
static char *ical_read_note(char *line, ical_property_e property, unsigned *noskipped,
ical_types_e item_type, const int itemline,
FILE * log)
{
char *p, *notestr, *note;
const int EOL = 1,
INDENT = (property != DESCRIPTION);
char *p, *pname, *notestr;
switch (property) {
case DESCRIPTION:
pname = "description";
break;
case LOCATION:
pname = "location";
break;
case COMMENT:
pname = "comment";
break;
case STATUS:
pname = "status";
break;
default:
pname = "no property";
}
p = ical_get_value(line);
if (!p) {
ical_log(log, item_type, itemline,
_("malformed description line."));
asprintf(&p, _("malformed %s line."), pname);
ical_log(log, item_type, itemline, p);
mem_free(p);
(*noskipped)++;
return NULL;
notestr = NULL;
goto leave;
}
notestr = ical_unformat_line(p);
notestr = ical_unformat_line(p, EOL, INDENT);
if (!notestr) {
ical_log(log, item_type, itemline, _("malformed description."));
asprintf(&p, _("malformed %s."), pname);
ical_log(log, item_type, itemline, p);
mem_free(p);
(*noskipped)++;
return NULL;
} else {
note = generate_note(notestr);
mem_free(notestr);
return note;
}
leave:
return notestr;
}
/* Returns an allocated string containing the ical item summary. */
@ -911,30 +948,31 @@ static char *ical_read_summary(char *line, unsigned *noskipped,
ical_types_e item_type, const int itemline,
FILE * log)
{
char *p, *summary;
const int EOL = 0, INDENT = 0;
char *p, *summary = NULL;
p = ical_get_value(line);
if (!p) {
ical_log(log, item_type, itemline, _("malformed summary line"));
ical_log(log, item_type, itemline, _("malformed summary line."));
(*noskipped)++;
return NULL;
goto leave;
}
summary = ical_unformat_line(p);
summary = ical_unformat_line(p, EOL, INDENT);
if (!summary) {
ical_log(log, item_type, itemline, _("malformed summary."));
(*noskipped)++;
return NULL;
goto leave;
}
/* Event summaries must not contain newlines. */
/* An event summary is one line only. */
if (strchr(summary, '\n')) {
ical_log(log, item_type, itemline, _("line break in summary."));
(*noskipped)++;
mem_free(summary);
return NULL;
summary = NULL;
}
leave:
return summary;
}
@ -946,21 +984,26 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
{
const int ITEMLINE = *lineno - !feof(fdi);
ical_vevent_e vevent_type;
char *p;
ical_property_e property;
char *p, *note = NULL, *comment;
const char *SEPARATOR = "-- \n";
struct string s;
struct {
llist_t exc;
ical_rpt_t *rpt;
char *mesg, *note;
char *mesg, *desc, *loc, *comm, *stat, *note;
long start, end, dur;
int has_alarm;
} vevent;
int skip_alarm;
int skip_alarm, has_note, separator;
vevent_type = UNDEFINED;
memset(&vevent, 0, sizeof vevent);
LLIST_INIT(&vevent.exc);
skip_alarm = 0;
skip_alarm = has_note = separator = 0;
while (ical_readline(fdi, buf, lstore, lineno)) {
note = NULL;
property = NO_PROPERTY;
if (skip_alarm) {
/*
* Need to skip VALARM properties because some keywords
@ -970,7 +1013,6 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
skip_alarm = 0;
continue;
}
if (starts_with_ci(buf, "END:VEVENT")) {
if (!vevent.mesg) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
@ -982,7 +1024,6 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
_("item start date is not defined."));
goto skip;
}
if (vevent_type == APPOINTMENT && vevent.dur == 0) {
if (vevent.end != 0) {
vevent.dur = vevent.end - vevent.start;
@ -994,13 +1035,38 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
goto skip;
}
}
if (vevent.rpt && vevent.rpt->count) {
vevent.rpt->until =
ical_compute_rpt_until(vevent.start,
vevent.rpt);
}
if (has_note) {
/* Construct string with note file contents. */
string_init(&s);
if (vevent.desc) {
string_catf(&s, "%s", vevent.desc);
mem_free(vevent.desc);
if (separator)
string_catf(&s, SEPARATOR);
}
if (vevent.loc) {
string_catf(&s, _("Location: %s"),
vevent.loc);
mem_free(vevent.loc);
}
if (vevent.comm) {
string_catf(&s, _("Comment: %s"),
vevent.comm);
mem_free(vevent.comm);
}
if (vevent.stat) {
string_catf(&s, _("Status: %s"),
vevent.stat);
mem_free(vevent.stat);
}
vevent.note = generate_note(string_buf(&s));
mem_free(s.buf);
}
switch (vevent_type) {
case APPOINTMENT:
ical_store_apoint(vevent.mesg, vevent.note,
@ -1023,10 +1089,8 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
goto skip;
break;
}
return;
}
if (starts_with_ci(buf, "DTSTART")) {
p = ical_get_value(buf);
if (!p) {
@ -1079,23 +1143,84 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
} else if (starts_with_ci(buf, "BEGIN:VALARM")) {
skip_alarm = vevent.has_alarm = 1;
} else if (starts_with_ci(buf, "DESCRIPTION")) {
vevent.note = ical_read_note(buf, noskipped,
property = DESCRIPTION;
} else if (starts_with_ci(buf, "LOCATION")) {
property = LOCATION;
} else if (starts_with_ci(buf, "COMMENT")) {
property = COMMENT;
} else if (starts_with_ci(buf, "STATUS")) {
property = STATUS;
}
if (property) {
note = ical_read_note(buf, property, noskipped,
ICAL_VEVENT, ITEMLINE, log);
if (!vevent.note)
if (!note)
goto cleanup;
separator = (property != DESCRIPTION);
has_note = 1;
}
switch (property) {
case DESCRIPTION:
if (vevent.desc) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("only one description allowed."));
goto skip;
}
vevent.desc = note;
break;
case LOCATION:
if (vevent.loc) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("only one location allowed."));
goto skip;
}
vevent.loc = note;
break;
case COMMENT:
/* There may be more than one. */
if (vevent.comm) {
asprintf(&comment, "%sComment: %s",
vevent.comm, note);
mem_free(vevent.comm);
vevent.comm = comment;
} else
vevent.comm = note;
break;
case STATUS:
if (vevent.stat) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("only one status allowed."));
goto skip;
}
if (!(starts_with(note, "TENTATIVE") ||
starts_with(note, "CONFIRMED") ||
starts_with(note, "CANCELLED"))) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("invalid status value."));
goto skip;
}
vevent.stat = note;
break;
default:
break;
}
}
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("The ical file seems to be malformed. "
"The end of item was not found."));
skip:
(*noskipped)++;
cleanup:
if (vevent.note)
mem_free(vevent.note);
if (note)
mem_free(note);
if (vevent.desc)
mem_free(vevent.desc);
if (vevent.loc)
mem_free(vevent.loc);
if (vevent.comm)
mem_free(vevent.comm);
if (vevent.stat)
mem_free(vevent.stat);
if (vevent.mesg)
mem_free(vevent.mesg);
if (vevent.rpt)
@ -1108,16 +1233,22 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
char *buf, char *lstore, unsigned *lineno, const char *fmt_todo)
{
const int ITEMLINE = *lineno - !feof(fdi);
ical_property_e property;
char *note = NULL, *comment;
const char *SEPARATOR = "-- \n";
struct string s;
struct {
char *mesg, *note;
char *mesg, *desc, *loc, *comm, *stat, *note;
int priority;
int completed;
} vtodo;
int skip_alarm;
int skip_alarm, has_note, separator;
memset(&vtodo, 0, sizeof vtodo);
skip_alarm = 0;
skip_alarm = has_note = separator = 0;
while (ical_readline(fdi, buf, lstore, lineno)) {
note = NULL;
property = NO_PROPERTY;
if (skip_alarm) {
/*
* Need to skip VALARM properties because some keywords
@ -1127,20 +1258,44 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
skip_alarm = 0;
continue;
}
if (starts_with_ci(buf, "END:VTODO")) {
if (!vtodo.mesg) {
ical_log(log, ICAL_VTODO, ITEMLINE,
_("could not retrieve item summary."));
goto cleanup;
}
if (has_note) {
/* Construct string with note file contents. */
string_init(&s);
if (vtodo.desc) {
string_catf(&s, "%s", vtodo.desc);
mem_free(vtodo.desc);
if (separator)
string_catf(&s, SEPARATOR);
}
if (vtodo.loc) {
string_catf(&s, _("Location: %s"),
vtodo.loc);
mem_free(vtodo.loc);
}
if (vtodo.comm) {
string_catf(&s, _("Comment: %s"),
vtodo.comm);
mem_free(vtodo.comm);
}
if (vtodo.stat) {
string_catf(&s, _("Status: %s"),
vtodo.stat);
mem_free(vtodo.stat);
}
vtodo.note = generate_note(string_buf(&s));
mem_free(s.buf);
}
ical_store_todo(vtodo.priority, vtodo.completed,
vtodo.mesg, vtodo.note, fmt_todo);
(*notodos)++;
return;
}
if (starts_with_ci(buf, "PRIORITY:")) {
sscanf(buf, "PRIORITY:%d\n", &vtodo.priority);
if (vtodo.priority < 0 || vtodo.priority > 9) {
@ -1151,6 +1306,7 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
}
} else if (starts_with_ci(buf, "STATUS:COMPLETED")) {
vtodo.completed = 1;
property = STATUS;
} else if (starts_with_ci(buf, "SUMMARY")) {
vtodo.mesg =
ical_read_summary(buf, noskipped, ICAL_VTODO,
@ -1160,22 +1316,85 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
} else if (starts_with_ci(buf, "BEGIN:VALARM")) {
skip_alarm = 1;
} else if (starts_with_ci(buf, "DESCRIPTION")) {
vtodo.note = ical_read_note(buf, noskipped, ICAL_VTODO,
ITEMLINE, log);
if (!vtodo.note)
property = DESCRIPTION;
} else if (starts_with_ci(buf, "LOCATION")) {
property = LOCATION;
} else if (starts_with_ci(buf, "COMMENT")) {
property = COMMENT;
} else if (starts_with_ci(buf, "STATUS")) {
property = STATUS;
}
if (property) {
note = ical_read_note(buf, property, noskipped,
ICAL_VTODO, ITEMLINE, log);
if (!note)
goto cleanup;
separator = (property != DESCRIPTION);
has_note = 1;
}
switch (property) {
case DESCRIPTION:
if (vtodo.desc) {
ical_log(log, ICAL_VTODO, ITEMLINE,
_("only one description allowed."));
goto skip;
}
vtodo.desc = note;
break;
case LOCATION:
if (vtodo.loc) {
ical_log(log, ICAL_VTODO, ITEMLINE,
_("only one location allowed."));
goto skip;
}
vtodo.loc = note;
break;
case COMMENT:
/* There may be more than one. */
if (vtodo.comm) {
asprintf(&comment, "%sComment: %s",
vtodo.comm, note);
mem_free(vtodo.comm);
vtodo.comm = comment;
} else
vtodo.comm = note;
break;
case STATUS:
if (vtodo.stat) {
ical_log(log, ICAL_VTODO, ITEMLINE,
_("only one status allowed."));
goto skip;
}
if (!(starts_with(note, "NEEDS-ACTION") ||
starts_with(note, "COMPLETED") ||
starts_with(note, "IN-PROCESS") ||
starts_with(note, "CANCELLED"))) {
ical_log(log, ICAL_VTODO, ITEMLINE,
_("invalid status value."));
goto skip;
}
vtodo.stat = note;
break;
default:
break;
}
}
ical_log(log, ICAL_VTODO, ITEMLINE,
_("The ical file seems to be malformed. "
"The end of item was not found."));
skip:
(*noskipped)++;
cleanup:
if (vtodo.note)
mem_free(vtodo.note);
if (note)
mem_free(note);
if (vtodo.desc)
mem_free(vtodo.desc);
if (vtodo.loc)
mem_free(vtodo.loc);
if (vtodo.comm)
mem_free(vtodo.comm);
if (vtodo.stat)
mem_free(vtodo.stat);
if (vtodo.mesg)
mem_free(vtodo.mesg);
}

View File

@ -59,13 +59,9 @@ HTABLE_PROTOTYPE(htp, note_gc_hash)
char *generate_note(const char *str)
{
char *sha1 = mem_malloc(SHA1_DIGESTLEN * 2 + 1);
char *notepath, *s;
char *notepath;
FILE *fp;
/* Temporary hack */
asprintf(&s, "%s\n", str);
str = s;
sha1_digest(str, sha1);
asprintf(&notepath, "%s%s", path_notes, sha1);
fp = fopen(notepath, "w");
@ -74,7 +70,6 @@ char *generate_note(const char *str)
fputs(str, fp);
file_close(fp, __FILE_POS__);
mem_free(s);
mem_free(notepath);
return sha1;
}

View File

@ -59,6 +59,7 @@ TESTS = \
ical-009.sh \
ical-010.sh \
ical-011.sh \
ical-012.sh \
next-001.sh \
next-002.sh \
next-003.sh \
@ -134,5 +135,6 @@ EXTRA_DIST = \
data/ical-007.ical \
data/ical-008.ical \
data/ical-009.ical \
data/ical-012.ical \
data/todo \
data/todo-export

View File

@ -63,6 +63,19 @@ BEGIN:VTODO
PRIORITY:1
SUMMARY:an unescaped comma: ,
END:VTODO
BEGIN:VEVENT
DTSTART:20200406T221300
DURATION:PT0H15M0S
SUMMARY:Invalid STATUS
STATUS:confirmed
END:VEVENT
BEGIN:VEVENT
DTSTART:20200406T221300
DURATION:PT0H15M0S
SUMMARY:LOCATION twice
LOCATION:first
LOCATION:second
END:VEVENT
BEGIN:VTODO
SUMMARY:finally\, missing end of item
END:VCALENDAR

93
test/data/ical-012.ical Normal file
View File

@ -0,0 +1,93 @@
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. Comment and status
COMMENT:Event without description: a comment\nstreching over\nthree lines
STATUS:CONFIRMED
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\, status\, comment and location
DESCRIPTION:event with\ndescription\nstatus\ncomment\nand location
LOCATION: Right here
COMMENT:just a repetition of description:\nevent with\ndescription\nstatus\ncomment\nand location
STATUS:CANCELLED
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:Comment and status
COMMENT:Todo with out description. A comment\nstreching over\nthree lines
STATUS:NEEDS-ACTION
END:VTODO
BEGIN:VTODO
PRIORITY:5
SUMMARY:Empty description
DESCRIPTION:
END:VTODO
BEGIN:VTODO
PRIORITY:6
SUMMARY:Empty description\,but status
DESCRIPTION:
STATUS:COMPLETED
END:VTODO
BEGIN:VTODO
SUMMARY:todo with description\, status\, comment and location
DESCRIPTION:todo with\ndescription\nstatus\ncomment\nand location\,\nbut no priority
LOCATION: Right here
COMMENT:mostly a repetition of description:\ntodo with\ndescription\nstatus\ncomment\nand location
STATUS:IN-PROCESS
END:VTODO
END:VCALENDAR

View File

@ -7,26 +7,30 @@ if [ "$1" = 'actual' ]; then
mkdir .calcurse || exit 1
cp "$DATA_DIR/conf" .calcurse || exit 1
out=$("$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-009.ical" 2>&1)
echo "$out" | sed -n '4,5p'
log=$(echo "$out" | awk '$1 == "See" {print $2}')
cat "$log" | sed '1,17d'
cat $PWD/.calcurse/notes/* | wc
# Print the import report (stdout).
echo "$out" | awk '$1 == "Import"; $2 == "apps"'
# Find the log file and print the log messages (stderr).
logfile=$(echo "$out" | awk '$1 == "See" { print $2 }')
sed '1,18d' "$logfile"
# One empty note file.
cat "$PWD/.calcurse/notes"/* | wc | awk '{ print $1 $2 $3 }'
rm -rf .calcurse || exit 1
elif [ "$1" = 'expected' ]; then
cat <<EOD
Import process report: 0068 lines read
2 apps / 0 events / 1 todo / 10 skipped
Import process report: 0081 lines read
2 apps / 0 events / 1 todo / 12 skipped
VEVENT [12]: could not retrieve event start time.
VEVENT [17]: recurrence frequency not recognized.
VEVENT [23]: malformed summary line
VEVENT [23]: malformed summary line.
VTODO [28]: item priority is invalid (must be between 0 and 9).
VEVENT [32]: malformed exceptions line.
VEVENT [39]: line break in summary.
VEVENT [44]: malformed description line.
VEVENT [50]: malformed description.
VTODO [62]: malformed summary.
VTODO [66]: The ical file seems to be malformed. The end of item was not found.
VEVENT [66]: invalid status value.
VEVENT [72]: only one location allowed.
VTODO [79]: The ical file seems to be malformed. The end of item was not found.
101
EOD
else

76
test/ical-012.sh Executable file
View File

@ -0,0 +1,76 @@
#!/bin/sh
# Note file creation. Eleven note files are created for 6 apps and 6 todos.
# To produce a fixed, predictable directory listing it is necessary that the
# notes are of different sizes (except for the vevent and vtodo empty note which
# is shared).
. "${TEST_INIT:-./test-init.sh}"
if [ "$1" = 'actual' ]; then
mkdir .calcurse &&
cp "$DATA_DIR/conf" .calcurse || exit 1
"$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-012.ical"
(cd "$PWD/.calcurse/notes/"; cat $(ls -S1))
rm -rf .calcurse || exit 1
elif [ "$1" = 'expected' ]; then
cat <<EOD
Import process report: 0093 lines read
6 apps / 0 events / 6 todos / 0 skipped
todo with
description
status
comment
and location,
but no priority
--
Location: Right here
Comment: mostly a repetition of description:
todo with
description
status
comment
and location
Status: IN-PROCESS
event with
description
status
comment
and location
--
Location: Right here
Comment: just a repetition of description:
event with
description
status
comment
and location
Status: CANCELLED
Comment: Todo with out description. A comment
streching over
three lines
Status: NEEDS-ACTION
Comment: Event without description: a comment
streching over
three lines
Status: CONFIRMED
event with description
and location
--
Location: Right here
todo with description
and location
--
Location: Right here
--
Comment: event with empty description
event with one-line description
todo with one-line description
--
Status: COMPLETED
EOD
else
./run-test "$0"
fi