summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--awk/index.awk100
-rw-r--r--tests/index/review_data.org25
-rw-r--r--tests/org-fc-review-data-test.el53
3 files changed, 156 insertions, 22 deletions
diff --git a/awk/index.awk b/awk/index.awk
index 638fb70..1d5f003 100644
--- a/awk/index.awk
+++ b/awk/index.awk
@@ -1,7 +1,35 @@
+### Commentary
+#
+# This file implements a parser that reads org files, extracts data
+# relevant to org-fc and prints it as an S-expression so it can be
+# parsed with EmacsLisp's read function.
+#
+# The org format is mostly line based.
+# A small state machine is used to keep track of where we are in a file,
+# (e.g. inside a card, reading heading properties, reading review data).
+#
+# Some parsing of review data columns is done.
+#
+# The position is escaped as a string and the due date is converted
+# into Emacs's date format because it's a bit faster in AWK than in
+# EmacsLisp.
+#
+# All other columns of the review data table are assumed to be numeric
+# values and included in the output S-expression without any escaping.
+#
+# Because of the complicated rules used by org-mode to determine a
+# heading's tags, inherited (file / parent heading) and local tags are
+# tracked separately and later combined using an org-mode function.
+#
+### Code
+
BEGIN {
# The only time we're interested in multiple fields is when
# parsing the review data drawer.
- FS="|";
+ #
+ # Treating whitespace as part of the field separator instead of
+ # stripping it from the fields afterwards is a bit faster.
+ FS="[ \t]*|[ \t]*";
now = strftime("%FT%TZ", systime(), 1);
@@ -19,7 +47,8 @@ BEGIN {
state_properties = 2;
state_properties_done = 3;
state_review_data = 4;
- state_review_data_done = 5;
+ state_review_data_body = 5;
+ state_review_data_done = 6;
print "(";
}
@@ -39,7 +68,9 @@ BEGINFILE {
}
ENDFILE {
- print ") :title " (file_title ? escape_string(file_title) : "nil") ")";
+ # On `BEGINFILE` we don't know the file's title yet so we output
+ # it once done processing the rest of the file.
+ print " ) :title " (file_title ? escape_string(file_title) : "nil") ")";
}
## File Tags
@@ -99,8 +130,12 @@ match($0, /^(\*+)[ \t]+(.*)$/, a) {
$0 ~ review_data_drawer {
# Make sure the review data comes after the property drawer
if (state == state_properties_done) {
+ delete review_data_columns;
+ review_data_ncolumns = 0;
+
delete review_data;
review_index = 1;
+
state = state_review_data;
}
next;
@@ -109,7 +144,7 @@ $0 ~ review_data_drawer {
/:END:/ {
if (state == state_properties) {
state = state_properties_done;
- } else if (state == state_review_data) {
+ } else if (state == state_review_data_body) {
state = state_review_data_done;
# Card header
inherited_tags = "";
@@ -135,13 +170,21 @@ $0 ~ review_data_drawer {
# Card positions
for (i = 1; i < review_index; i++) {
- print " (" \
- ":position " escape_string(review_data[i]["position"]) \
- " :ease " review_data[i]["ease"] \
- " :box " review_data[i]["box"] \
- " :interval " review_data[i]["interval"] \
- " :due " parse_time(review_data[i]["due"]) \
- ")"
+ print " (";
+ for (j = 1; j <= review_data_ncolumns; j++) {
+ col = review_data_columns[j];
+ val = review_data[i][col];
+
+ # TODO: extract values as strings, parse in Emacs when
+ # necessary.
+ if (col == "due") {
+ val = parse_time(val);
+ } else if (col == "position") {
+ val = escape_string(val);
+ }
+ print " :" col " " val;
+ }
+ print " )";
}
print " ))";
}
@@ -157,19 +200,32 @@ $0 ~ review_data_drawer {
## Review data parsing
-# TODO: Explicit match, to check for broken drawers
-#
+# Table separator
+(state == state_review_data) && /^\|[-+]+\|$/ {
+ state = state_review_data_body;
+ next;
+}
+
+# Column Names
+# NOTE: This line comes before the table separator in the file but to
+# keep the regex simple, we match it later.
+(state == state_review_data) && /^\|.*\|$/ {
+ # Skip the first and last empty fields
+ for (i = 2; i <= (NF - 1); i++) {
+ review_data_columns[i - 1] = $i;
+ }
+ review_data_ncolumns = NF - 2;
+ next;
+}
+
# Positions are collected in an array first,
# in case the review drawer is broken.
-(state == state_review_data) && /^\|.*\|$/ {
- # check NF to skip the |--+--| table separator
- # match on $2 to skip the table header
- if (NF == 7 && $2 !~ "position") {
- review_data[review_index]["position"] = trim($2);
- review_data[review_index]["ease"] = trim($3);
- review_data[review_index]["box"] = trim($4);
- review_data[review_index]["interval"] = trim($5);
- review_data[review_index]["due"] = trim_surrounding($6);
+(state == state_review_data_body) && /^\|.*\|$/ {
+ if (NF == (review_data_ncolumns + 2)) {
+ for (i = 2; i <= (NF - 1); i++) {
+ column = review_data_columns[i - 1];
+ review_data[review_index][column] = $i;
+ }
review_index += 1;
}
next;
diff --git a/tests/index/review_data.org b/tests/index/review_data.org
new file mode 100644
index 0000000..84fb15a
--- /dev/null
+++ b/tests/index/review_data.org
@@ -0,0 +1,25 @@
+* SM2 :fc:
+:PROPERTIES:
+:FC_CREATED: 2020-11-06T10:40:17Z
+:FC_TYPE: double
+:ID: f8cc05c7-aa3a-4a21-aa71-38178477e619
+:END:
+:REVIEW_DATA:
+| position | ease | box | interval | due |
+|----------+------+-----+----------+----------------------|
+| front | 2.5 | 0 | 0 | 2020-11-06T10:40:17Z |
+| back | 2.8 | 2 | 123.4 | 2020-11-06T10:40:20Z |
+:END:
+Back
+* Alternative :fc:
+:PROPERTIES:
+:FC_CREATED: 2020-11-06T11:31:05Z
+:FC_TYPE: double
+:ID: 404557e5-ec07-4ee1-a000-3f0e8a94eaa0
+:END:
+:REVIEW_DATA:
+| position | custom1 | custom2 | due |
+|----------+---------+---------+----------------------|
+| front | 1.0 | 3 | 2020-11-06T11:31:05Z |
+| back | 2.0 | 4 | 2020-11-06T11:31:10Z |
+:END:
diff --git a/tests/org-fc-review-data-test.el b/tests/org-fc-review-data-test.el
new file mode 100644
index 0000000..c00083b
--- /dev/null
+++ b/tests/org-fc-review-data-test.el
@@ -0,0 +1,53 @@
+(require 'org-fc)
+(require 'org-fc-test-helper)
+(require 'ert)
+
+(ert-deftest org-fc-test-review-data ()
+ (let ((index (org-fc-awk-index-paths
+ (list
+ (org-fc-test-fixture "index/review_data.org")))))
+ (should (eq (length index) 2))
+ (let ((card1 (car index))
+ (card2 (cadr index)))
+
+ (should
+ (equal (plist-get card1 :id)
+ "f8cc05c7-aa3a-4a21-aa71-38178477e619"))
+ (should
+ (eq (length (plist-get card1 :positions)) 2))
+
+ (let* ((poss (plist-get card1 :positions))
+ (pos1 (car poss))
+ (pos2 (cadr poss)))
+
+ (should (equal (plist-get pos1 :position) "front"))
+ (should (equal (plist-get pos1 :ease) 2.5))
+ (should (equal (plist-get pos1 :box) 0))
+ (should (equal (plist-get pos1 :interval) 0))
+ (should (equal (plist-get pos1 :due) '(24485 10257)))
+
+ (should (equal (plist-get pos2 :position) "back"))
+ (should (equal (plist-get pos2 :ease) 2.8))
+ (should (equal (plist-get pos2 :box) 2))
+ (should (equal (plist-get pos2 :interval) 123.4))
+ (should (equal (plist-get pos2 :due) '(24485 10260))))
+
+ (should
+ (equal (plist-get card2 :id)
+ "404557e5-ec07-4ee1-a000-3f0e8a94eaa0"))
+ (should
+ (eq (length (plist-get card2 :positions)) 2))
+
+ (let* ((poss (plist-get card2 :positions))
+ (pos1 (car poss))
+ (pos2 (cadr poss)))
+
+ (should (equal (plist-get pos1 :position) "front"))
+ (should (equal (plist-get pos1 :due) '(24485 13305)))
+ (should (equal (plist-get pos1 :custom1) 1.0))
+ (should (equal (plist-get pos1 :custom2) 3))
+
+ (should (equal (plist-get pos2 :position) "back"))
+ (should (equal (plist-get pos2 :due) '(24485 13310)))
+ (should (equal (plist-get pos2 :custom1) 2.0))
+ (should (equal (plist-get pos2 :custom2) 4))))))