From 78d098346497874cff2e61c41eee694c27b305d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84min=20Baumeler?= Date: Fri, 4 Jul 2025 11:56:27 +0200 Subject: [PATCH] impr: handling of categories --- src/awk/export.awk | 24 ------------------ src/awk/get.awk | 31 +++++++++++++++++++++-- src/awk/list.awk | 6 +++++ src/awk/new.awk | 29 ++++++++++++++------- src/awk/update.awk | 53 ++++++++++++++++++++++++--------------- src/lib/awk/icalendar.awk | 13 ++++++---- src/sh/awkscripts.sh | 7 ------ src/sh/icalendar.sh | 11 +++++++- 8 files changed, 106 insertions(+), 68 deletions(-) delete mode 100644 src/awk/export.awk diff --git a/src/awk/export.awk b/src/awk/export.awk deleted file mode 100644 index a7f816f..0000000 --- a/src/awk/export.awk +++ /dev/null @@ -1,24 +0,0 @@ -@include "lib/awk/icalendar.awk" - -BEGIN { FS = "[:;]"; } -/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } -/^END:/ && $2 == type { exit } -/^(CATEGORIES|DESCRIPTION|SUMMARY|DUE)/ { prop = $1; c[prop] = $0; next; } -/^[^ ]/ && prop { prop = ""; next; } -/^ / && prop { c[prop] = c[prop] substr($0, 2); next; } -END { - if (!type) - exit - # Process content lines, force CATEGORIES and SUMMARY as single-line - c["CATEGORIES"] = singleline(getcontent(c["CATEGORIES"])) - c["DESCRIPTION"] = getcontent(c["DESCRIPTION"]) - c["SUMMARY"] = singleline(getcontent(c["SUMMARY"])) - c["DUE"] = getcontent(c["DUE"]) - # Print - if (c["DUE"]) - print "::: <| " substr(c["DUE"], 1, 4) "-" substr(c["DUE"], 5, 2) "-" substr(c["DUE"], 7, 2); - print "# " c["SUMMARY"]; - print "> " c["CATEGORIES"]; - print ""; - print c["DESCRIPTION"]; -} diff --git a/src/awk/get.awk b/src/awk/get.awk index abbeb9b..e45b09c 100644 --- a/src/awk/get.awk +++ b/src/awk/get.awk @@ -1,7 +1,17 @@ +# Retrieve content from iCalendar file +# +# Mandatory variable: `field`. +# Name of field to retrieve. +# +# Optional variable: `format`. +# If `format` is set to "csv", then the content is interpreted as +# comma-separated values, and empty values are dropped. +# If `format` is set to "date", then the content is interpreted as +# a date the output is in the form YYYY-MM-DD. @include "lib/awk/icalendar.awk" # print content of field `field` -BEGIN { FS = ":"; regex = "^" field; } +BEGIN { FS = ":"; ORS = ""; regex = "^" field; } /^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } /^END:/ && $2 == type { exit } $0 ~ field { line = $0; next; } @@ -10,5 +20,22 @@ $0 ~ field { line = $0; next; } END { if (!type) { exit } # Process line - print getcontent(line) + content = getcontent(line) + switch (format) { + case "csv" : + split(content, a, ",") + for (i in a) { + if (a[i]) + res = res "," a[i] + } + print substr(res, 2) + break + case "date" : + if (content) + print substr(parse_dt("", content), 1, 10) + break + default : + print content + break + } } diff --git a/src/awk/list.awk b/src/awk/list.awk index c9fc505..b0ad780 100644 --- a/src/awk/list.awk +++ b/src/awk/list.awk @@ -131,6 +131,12 @@ ENDFILE { # Process content lines # strings cat = singleline(unescape(getcontent(c["CATEGORIES"]))) + split(cat, a, ",") + cat = "" + for (i in a) + if (a[i]) + cat = cat "," a[i] + cat = substr(cat, 2) sta = singleline(unescape(getcontent(c["STATUS"]))) sum = singleline(unescape(getcontent(c["SUMMARY"]))) diff --git a/src/awk/new.awk b/src/awk/new.awk index d1e52c1..05b33c4 100644 --- a/src/awk/new.awk +++ b/src/awk/new.awk @@ -4,25 +4,36 @@ BEGIN { FS=":"; zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1); } -desc { desc = desc "\\n" escape($0); next; } -/^::: \|>/ && !start { gsub("\"", ""); start = substr(zulu, 1, 8); next; } -/^::: <\| / && !due { gsub("\"", ""); due = substr($0, 8); next; } -/^# / && !summary { summary = escape(substr($0, 3)); next; } -/^> / && !categories { categories = escape_but_commas(substr($0, 3)); next; } +desc { desc = desc "\\n" escape($0); next; } +/^::: \|>/ && !start { gsub("\"", ""); start = substr(zulu, 1, 8); next; } +/^::: <\|/ && !due { gsub("\"", ""); due = "D" substr($0, 8); next; } +/^# / && !summary { summary = "S" escape(substr($0, 3)); next; } +/^> / && !categories { categories = "C" escape_but_commas(substr($0, 3)); next; } !$0 && !el { el = 1; next; } !el { print "Unrecognized header on line "NR": " $0 > "/dev/stderr"; exit 1; } - { desc = "D" escape($0); next; } + { desc = "D" escape($0); next; } END { # Sanitize input type = due ? "VTODO" : "VJOURNAL" + due = substr(due, 2) + summary = substr(summary, 2) + categories = substr(categories, 2) + desc = substr(desc, 2) + if (categories) { + split(categories, a, ",") + categories = "" + for (i in a) + if (a[i]) + categories = categories "," a[i] + categories = substr(categories, 2) + } if (due) { # Use command line `date` for parsing cmd = "date -d \"" due "\" +\"%Y%m%d\""; - suc = cmd | getline res + suc = cmd | getline due close(cmd) if (suc != 1) exit 1 - due = res ? res : "" } # print ical @@ -52,7 +63,7 @@ END { } if (summary) print_fold("SUMMARY:", summary); if (categories) print_fold("CATEGORIES:", categories); - if (desc) print_fold("DESCRIPTION:", substr(desc, 2)); + if (desc) print_fold("DESCRIPTION:", desc); print "END:" type; print "END:VCALENDAR" } diff --git a/src/awk/update.awk b/src/awk/update.awk index 97b5ae5..6b21f35 100644 --- a/src/awk/update.awk +++ b/src/awk/update.awk @@ -6,30 +6,43 @@ BEGIN { } ENDFILE { - if (NR == FNR && due) { - # Use command line `date` for parsing - cmd = "date -d \"" due "\" +\"%Y%m%d\""; - suc = cmd | getline res - close(cmd) - if (suc != 1) - exit 1 - due = res ? res : "" + if (NR == FNR) { + due = substr(due, 2) + summary = substr(summary, 2) + categories = substr(categories, 2) + desc = substr(desc, 2) + if (categories) { + split(categories, a, ",") + categories = "" + for (i in a) + if (a[i]) + categories = categories "," a[i] + categories = substr(categories, 2) + } + if (due) { + # Use command line `date` for parsing + cmd = "date -d \"" due "\" +\"%Y%m%d\""; + suc = cmd | getline due + close(cmd) + if (suc != 1) + exit 1 + } } } -NR == FNR && desc { desc = desc "\\n" escape($0); next; } -NR == FNR && /^::: <\| / && !due { gsub("\"",""); due = substr($0, 8); next; } -NR == FNR && /^# / && !summary { summary = escape(substr($0, 3)); next; } -NR == FNR && /^> / && !categories { categories = escape_but_commas(substr($0, 3)); next; } -NR == FNR && !$0 && !el { el = 1; next; } +NR == FNR && desc { desc = desc "\\n" escape($0); next; } +NR == FNR && /^::: <\|/ && !due { gsub("\"",""); due = "D" substr($0, 8); next; } +NR == FNR && /^# / && !summary { summary = "S" escape(substr($0, 3)); next; } +NR == FNR && /^> / && !categories { categories = "C" escape_but_commas(substr($0, 3)); next; } +NR == FNR && !$0 && !el { el = 1; next; } NR == FNR && !el { print "Unrecognized header on line "NR": " $0 > "/dev/stderr"; exit 1; } -NR == FNR { desc = "D" escape($0); next; } +NR == FNR { desc = "D" escape($0); next; } due && type == "VJOURNAL" { print "Notes and journal entries do not have due dates." > "/dev/stderr"; exit 1; } -/^BEGIN:(VJOURNAL|VTODO)/ { type = $2; print; next; } -/^X-ALT-DESC/ && type { next; } # drop this alternative description -/^ / && type { next; } # drop this folded line (the only content with folded lines will be updated) -/^(DUE|SUMMARY|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && type { next; } # skip for now, we will write updated fields at the end -/^SEQUENCE/ && type { seq = $2; next; } # store sequence number and skip +/^BEGIN:(VJOURNAL|VTODO)/ { type = $2; print; next; } +/^X-ALT-DESC/ && type { next; } # drop this alternative description +/^ / && type { next; } # drop this folded line (the only content with folded lines will be updated) +/^(DUE|SUMMARY|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && type { next; } # skip for now, we will write updated fields at the end +/^SEQUENCE/ && type { seq = $2; next; } # store sequence number and skip /^END:/ && type == $2 { seq = seq ? seq + 1 : 1; print "SEQUENCE:" seq; @@ -37,7 +50,7 @@ due && type == "VJOURNAL" { print "Notes and journal entries do not have if (due) print "DUE;VALUE=DATE:" due; print_fold("SUMMARY:", summary); print_fold("CATEGORIES:", categories); - print_fold("DESCRIPTION:", substr(desc, 2)); + print_fold("DESCRIPTION:", desc); type = ""; } { print } diff --git a/src/lib/awk/icalendar.awk b/src/lib/awk/icalendar.awk index 4813209..9da0841 100644 --- a/src/lib/awk/icalendar.awk +++ b/src/lib/awk/icalendar.awk @@ -104,8 +104,11 @@ function getcontent(str) { # @local variables: tz # @input dt_param: iCalendar DTSTART or DTEND parameter string # @input dt_content: iCalendar DTSTART or DTEND content string -# @return: date or date-time string that can be used in date (1) -function parse_dt(dt_param, dt_content, tz, a, i, k) { +# @return: date or date-time string that can be used in date (1). In +# particular, date strings are of the form YYYY-MM-DD and datetime +# strings are of the form YYYY-MM-DD HH:MM:SS[Z]. If the field +# containts timezone information, then this is prepended. +function parse_dt(dt_param, dt_content, tz, a, i, k, date, time) { if (dt_param) { split(dt_param, a, ";") for (i in a) { @@ -117,9 +120,9 @@ function parse_dt(dt_param, dt_content, tz, a, i, k) { } } # Get date/date-time - return length(dt_content) == 8 ? - dt dt_content : - dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", dt_content) + date = substr(dt_content, 1, 4) "-" substr(dt_content, 5, 2) "-" substr(dt_content, 7, 2) + time = length(dt_content) == 8 ? "" : " " substr(dt_content, 10, 2) ":" substr(dt_content, 12, 2) ":" substr(dt_content, 14) + return tz date time } # Map iCalendar duration specification into the format to be used in date (1). diff --git a/src/sh/awkscripts.sh b/src/sh/awkscripts.sh index 144957d..e5a8e00 100644 --- a/src/sh/awkscripts.sh +++ b/src/sh/awkscripts.sh @@ -5,13 +5,6 @@ EOF ) export AWK_ALTERTODO -AWK_EXPORT=$( - cat <<'EOF' -@@include awk/export.awk -EOF -) -export AWK_EXPORT - AWK_GET=$( cat <<'EOF' @@include awk/get.awk diff --git a/src/sh/icalendar.sh b/src/sh/icalendar.sh index 27a4b9d..6564096 100644 --- a/src/sh/icalendar.sh +++ b/src/sh/icalendar.sh @@ -42,7 +42,16 @@ __edit() { file="$1" shift tmpmd=$(mktemp --suffix='.md') - awk "$AWK_EXPORT" "$file" >"$tmpmd" + due=$(awk -v field="DUE" -v format="date" "$AWK_GET" "$file") + if [ -n "$due" ]; then + echo "::: <| $due" >"$tmpmd" + fi + { + echo "# $(awk -v field="SUMMARY" "$AWK_GET" "$file" | tr '\n' ' ')" + echo "> $(awk -v field="CATEGORIES" -v format="csv" "$AWK_GET" "$file" | tr '\n' ' ')" + echo "" + awk -v field="DESCRIPTION" "$AWK_GET" "$file" + } >>"$tmpmd" checksum=$(cksum "$tmpmd") # Open in editor