Commit a38c0249 authored by ransome1's avatar ransome1
Browse files

Merge branch 'develop' of github.com:ransome1/sleek into develop

parents 20830e36 97f32d19
......@@ -97,12 +97,13 @@
"build:linux": "electron-builder -l --publish never",
"build:appx": "electron-builder -w appx --publish never",
"build:pacman": "electron-builder -l pacman --publish never",
"build:appimage": "yarn build:css && electron-builder -l AppImage --publish never",
"build:appimage": "yarn build:css && yarn build:pegjs && electron-builder -l AppImage --publish never",
"pack": "electron-builder --dir",
"lint": "eslint --ext .js, src --ext .mjs, src",
"test": "mocha --timeout 10000",
"test1": "mocha ./test/onboarding.js --timeout 10000",
"build:css": "sass src/scss/style.scss:src/css/style.css",
"build:pegjs": "peggy --format es --output src/js/filterlang.mjs src/js/filterlang.pegjs",
"sass": "sass -w src/scss/style.scss:src/css/style.css",
"start": "yarn sass & electron ."
},
......@@ -124,6 +125,7 @@
"electron-builder": "22.10.5",
"eslint": "^7.25.0",
"mocha": "^9.0.0",
"peggy": "^1.2.0",
"sass": "^1.34.1",
"spectron": "14.0.0"
}
......
{{
import { addIntervalToDate } from "./recurrences.mjs";
}}
filterQuery
= _ left:orExpr _ { return left; }
orExpr
= left:andExpr _ OrOp _ right:orExpr { return left.concat(right, ["||"]); }
/ left:andExpr { return left; }
andExpr
= left:notExpr _ AndOp _ right:andExpr { return left.concat(right, ["&&"]); }
/ left:notExpr { return left; }
notExpr
= NotOp _ left:notExpr { return left.concat(["!!"]); }
/ left:boolExpr { return left; }
boolExpr
= left:project { return left; }
/ left:context { return left; }
/ "(" _ left:orExpr _ ")" { return left; }
/ left:comparison { return left; }
/ "complete" { return ["complete"]; }
/ left:StringLiteral { return ["string", left]; }
/ left:RegexLiteral { return ["regex", left]; }
project
= "+" left:name { return ["++", left]; }
/ "+*" { return ["++", "*"]; }
context
= "@" left:name { return ["@@", left]; }
/ "@*" { return ["@@", "*"]; }
OrOp
= "||"
/ "or"i
AndOp
= "&&"
/ "and"i
NotOp
= "!"
/ "not"i
comparison
= left:priorityComparison { return left; }
/ left:dueComparison { return left; }
priorityComparison
= "priority" _ op:compareOp _ right:priorityLiteral { return ["priority", right, op]; }
/ "priority" { return ["priority"]; }
priorityLiteral
= [A-Z] { return text(); }
dueComparison
= "due" _ op:compareOp _ right:dateExpr { return ["due"].concat(right, [op]); }
/ "due" { return ["due"]; }
dateExpr
= left:dateLiteral _ op:dateOp _ count:number unit:[dbwmy] {
if (op == "-") {
count = count * -1;
}
// we do our date math with the same code as we use for
// recurrence calculations. All dates are returned from
// the parser as millisec since epoch (getTime()) to
// simplify comparisons in the filter lang execution engine.
let d = addIntervalToDate(new Date(left), count, unit);
return d.getTime();
}
/ left:dateLiteral { return left; }
dateOp
= "+" { return text(); }
/ "-" { return text(); }
compareOp
= "==" { return text(); }
/ "!=" { return text(); }
/ ">=" { return text(); }
/ "<=" { return text(); }
/ ">" { return text(); }
/ "<" { return text(); }
dateLiteral
= year:number4 "-" month: number2 "-" day:number2 {
let d = new Date(year, month-1, day);
return d.getTime();
}
/ "today" {
let d = new Date(); // now, w current time of day
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return d.getTime();
}
/ "tomorrow" {
let d = new Date(); // now, w current time of day
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return d.getTime() + 24*60*60*1000;
}
number4
= [0-9][0-9][0-9][0-9] { return text(); }
number2
= [0-9][0-9] { return text(); }
number
= [0-9]+ { return text(); }
StringLiteral "string"
= '"' chars:DoubleStringCharacter* '"' {
return chars.join("");
}
/ "'" chars:SingleStringCharacter* "'" {
return chars.join("");
}
DoubleStringCharacter
= '\\' '"' { return '"'; }
/ !'"' SourceCharacter { return text(); }
SingleStringCharacter
= '\\' "'" { return "'"; }
/ !"'" SourceCharacter { return text(); }
RegexLiteral "regex"
= "/" chars:RegexCharacter* "/" "i" {
return new RegExp(chars.join(""), "i");
}
/ "/" chars:RegexCharacter* "/" {
return new RegExp(chars.join(""));
}
RegexCharacter
= "\\" "/" { return "/"; }
/ !"/" SourceCharacter { return text(); }
SourceCharacter
= .
name
= [a-zA-Z_][a-zA-Z_0-9]* { return text(); }
_ "whitespace"
= [ \t\n\r]*
\ No newline at end of file
// This is a simple stack machine that executes a filter language query
// compiled by filterlang.pegjs (which generates filterlang.mjs).
// The compiled query consists of a list of postfix opcodes designed
// specifically for todo.txt searching and filtering.
function runQuery(item, compiledQuery) {
let stack = [];
let operand1 = false;
let operand2 = false;
let next = 0;
let q = compiledQuery.slice(); // shallow copy
while (q.length > 0) {
const opcode = q.shift();
switch(opcode) {
case "priority":
stack.push(item.priority);
break;
case "due":
let d = item.due;
if (d) {
// normalize date to have time of midnight in local zone
// we represent dates as millisec from epoch to simplify comparison
d = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
}
stack.push(d);
break;
case "complete":
stack.push(item.complete);
break;
case "string":
next = q.shift(); // the string value to match
stack.push(item.toString().toLowerCase().indexOf(next.toLowerCase()) !== -1);
break;
case "regex":
next = q.shift(); // the regex to match
stack.push(next.test(item.toString()));
break;
case "==":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 == operand2);
break;
case "!=":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 != operand2);
break;
case "<":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 < operand2);
break;
case "<=":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 <= operand2);
break;
case ">":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 > operand2);
break;
case ">=":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 >= operand2);
break;
case "++":
next = q.shift();
if (next == "*") {
stack.push(item.projects ? true : false);
} else {
stack.push(item.projects && item.projects.includes(next));
}
break;
case "@@":
next = q.shift();
if (next == "*") {
stack.push(item.contexts ? true : false);
} else {
stack.push(item.contexts && item.contexts.includes(next));
}
break;
case "||":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 || operand2);
break;
case "&&":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 && operand2);
break;
case "!!":
operand1 = stack.pop();
stack.push(!operand1);
break;
default:
// should be a data item like a string or date in millisec, ...
stack.push(opcode);
break;
}
}
return stack.pop();
}
export { runQuery };
......@@ -3,6 +3,8 @@ import { userData, handleError, translations, setUserData, startBuilding, getCon
import { _paq } from "./matomo.mjs";
import { items } from "./todos.mjs";
import { isToday, isPast, isFuture } from "./date.mjs";
import * as filterlang from "./filterlang.mjs";
import { runQuery } from "./filterquery.mjs";
const todoTableSearch = document.getElementById("todoTableSearch");
const autoCompleteContainer = document.getElementById("autoCompleteContainer");
......@@ -115,10 +117,25 @@ function filterItems(items) {
});
});
}
if (todoTableSearch.value && todoTableSearch.value.startsWith("?")) {
// if search starts with "?", parse it with filter query language grammar
try {
let query = filterlang.parse(todoTableSearch.value.slice(1));
if (query.length > 0) {
items = items.filter(function(item) {
return runQuery(item, query);
});
}
} catch(e) {
// if query is malformed, don't match anything, so user can tell that
// query is busted.
items = [];
}
}
// apply filters or filter by search string
items = items.filter(function(item) {
if(!item.text) return false
if(todoTableSearch.value && item.toString().toLowerCase().indexOf(todoTableSearch.value.toLowerCase()) === -1) return false;
if(todoTableSearch.value && !todoTableSearch.value.startsWith("?") && item.toString().toLowerCase().indexOf(todoTableSearch.value.toLowerCase()) === -1) return false;
if(!userData.showHidden && item.h) return false;
if(!userData.showCompleted && item.complete) return false;
if(!userData.showDueIsToday && item.due && isToday(item.due)) return false;
......
......@@ -64,19 +64,31 @@ function getDueDate(due, recurrence) {
// (Otherwise we will use the previous due date, for strict recurrence.)
due = new Date(); // use today's date as base for recurrence
}
return addIntervalToDate(due, recSplit.mul, recSplit.period);
}
// addIntervalToDate used to compute recurrences, but now also used to add
// or subtract a time interval to/from dates in filter query language.
// Therefore it must now handle negative count values.
function addIntervalToDate(due, count, unit) {
let days = 0;
let months = 0;
switch (recSplit.period) {
switch (unit) {
case "b":
// add "mul" business days, defined as not Sat or Sun
{
let bdays_left = recSplit.mul;
let incr = (count >= 0)? 1: -1;
let bdays_left = count * incr;
let millisec_due = due.getTime();
let day_of_week = due.getDay(); // 0=Sunday, 1..5 weekday, 6=Saturday
while (bdays_left > 0) {
millisec_due += 1000 * 60 * 60 * 24; // add a day to time
day_of_week = (day_of_week + 1)% 7; // new day of week
if (day_of_week != 0 && day_of_week != 6) {
millisec_due += 1000 * 60 * 60 * 24 * incr; // add a day to time
if (incr > 0) {
day_of_week = (day_of_week < 6 ? day_of_week + incr : 0);
} else {
day_of_week = (day_of_week > 0 ? day_of_week + incr : 6);
}
if (day_of_week > 0 && day_of_week < 6) {
bdays_left--; // one business day step accounted for!
}
}
......@@ -96,7 +108,7 @@ function getDueDate(due, recurrence) {
break;
}
if (months > 0) {
let due_month = due.getMonth() + recSplit.mul * months;
let due_month = due.getMonth() + count * months;
let due_year = due.getFullYear() + Math.floor(due_month/12);
due_month = due_month % 12;
let monthlen = new Date(due_year, due_month+1, 0).getDate();
......@@ -104,8 +116,8 @@ function getDueDate(due, recurrence) {
return new Date(due_year, due_month, due_day);
}
due = due.getTime();
due += 1000 * 60 * 60 * 24 * recSplit.mul * days;
due += 1000 * 60 * 60 * 24 * count * days;
return new Date(due);
}
export { splitRecurrence, generateRecurrence };
export { splitRecurrence, generateRecurrence, addIntervalToDate };
......@@ -2447,6 +2447,11 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
peggy@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/peggy/-/peggy-1.2.0.tgz#657ba45900cbef1dc9f52356704bdbb193c2021c"
integrity sha512-PQ+NKpAobImfMprYQtc4Egmyi29bidRGEX0kKjCU5uuW09s0Cthwqhfy7mLkwcB4VcgacE5L/ZjruD/kOPCUUw==
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment