From 28bd570436b8c14b1876f8eb6d461e58c8ebc718 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Sun, 10 Sep 2023 11:54:22 +0300 Subject: [PATCH] * can use weekday for duedate in smart syntax; can use ! and @ for duedate prefix (GH-26) --- src/includes/api/TasksController.php | 175 ++++-------------------- src/includes/common.php | 10 ++ src/includes/smartsyntax.php | 196 +++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 147 deletions(-) create mode 100644 src/includes/smartsyntax.php diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php index 4588b96..ad6aa6b 100644 --- a/src/includes/api/TasksController.php +++ b/src/includes/api/TasksController.php @@ -7,6 +7,7 @@ */ require_once(MTTINC. 'markup.php'); +require_once(MTTINC. 'smartsyntax.php'); class TasksController extends ApiController { @@ -240,7 +241,7 @@ class TasksController extends ApiController { 'tags' => '', 'duedate' => '', ); - if (Config::get('smartsyntax') != 0 && (false !== $a = $this->parseSmartSyntax($t['title']))) + if (Config::get('smartsyntax') != 0 && (false !== $a = parseSmartSyntax($t['title']))) { $t['title'] = (string) ($a['title'] ?? ''); $t['prio'] = (int) ($a['prio'] ?? 0); @@ -325,7 +326,7 @@ class TasksController extends ApiController { $duedate = null; if (Config::get('smartsyntax') != 0) { - $a = $this->parseSmartSyntax($title); + $a = parseSmartSyntax($title); if ($a === false) { return $t; } @@ -371,7 +372,7 @@ class TasksController extends ApiController { $prio = (int)($this->req->jsonBody['prio'] ?? 0); if ($prio < -1) $prio = -1; elseif ($prio > 2) $prio = 2; - $duedate = $this->parseDuedate(trim( $this->req->jsonBody['duedate'] ?? '' )); + $duedate = MTTSmartSyntax::parseDuedate(trim( $this->req->jsonBody['duedate'] ?? '' )); $t = array(); $t['total'] = 0; if ($title == '') { @@ -409,7 +410,7 @@ class TasksController extends ApiController { $prio = (int)($this->req->jsonBody['prio'] ?? 0); if ($prio < -1) $prio = -1; elseif ($prio > 2) $prio = 2; - $duedate = $this->parseDuedate(trim( $this->req->jsonBody['duedate'] ?? '' )); + $duedate = MTTSmartSyntax::parseDuedate(trim( $this->req->jsonBody['duedate'] ?? '' )); $t = array(); $t['total'] = 0; if ($title == '') { @@ -633,56 +634,6 @@ class TasksController extends ApiController { ); } - - private function parseDuedate($s): ?string - { - $df2 = Config::get('dateformat2'); - if (max((int)strpos($df2,'n'), (int)strpos($df2,'m')) > max((int)strpos($df2,'d'), (int)strpos($df2,'j'))) $formatDayFirst = true; - else $formatDayFirst = false; - - $y = $m = $d = 0; - if (preg_match("|^(\d+)-(\d+)-(\d+)\b|", $s, $ma)) { - $y = (int)$ma[1]; $m = (int)$ma[2]; $d = (int)$ma[3]; - } - elseif (preg_match("|^(\d+)\/(\d+)\/(\d+)\b|", $s, $ma)) - { - if($formatDayFirst) { - $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; - } else { - $m = (int)$ma[1]; $d = (int)$ma[2]; $y = (int)$ma[3]; - } - } - elseif (preg_match("|^(\d+)\.(\d+)\.(\d+)\b|", $s, $ma)) { - $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; - } - elseif (preg_match("|^(\d+)\.(\d+)\b|", $s, $ma)) { - $d = (int)$ma[1]; $m = (int)$ma[2]; - $a = explode(',', date('Y,m,d')); - if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; - else $y = (int)$a[0]; - } - elseif (preg_match("|^(\d+)\/(\d+)\b|", $s, $ma)) - { - if($formatDayFirst) { - $d = (int)$ma[1]; $m = (int)$ma[2]; - } else { - $m = (int)$ma[1]; $d = (int)$ma[2]; - } - $a = explode(',', date('Y,m,d')); - if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; - else $y = (int)$a[0]; - } - else return null; - if ($y < 100) $y = 2000 + $y; - elseif ($y < 1000 || $y > 2099) $y = 2000 + (int)substr((string)$y, -2); - if ($m > 12) $m = 12; - $maxdays = $this->daysInMonth($m,$y); - if ($m < 10) $m = '0'.$m; - if ($d > $maxdays) $d = $maxdays; - elseif ($d < 10) $d = '0'.$d; - return "$y-$m-$d"; - } - private function prepareDuedate($duedate): array { $lang = Lang::instance(); @@ -692,19 +643,30 @@ class TasksController extends ApiController { return $a; } $ad = explode('-', $duedate); - $at = explode('-', date('Y-m-d')); - $a['timestamp'] = mktime(0,0,0, (int)$ad[1], (int)$ad[2], (int)$ad[0]); - $diff = mktime(0,0,0, (int)$ad[1], (int)$ad[2], (int)$ad[0]) - mktime(0,0,0, (int)$at[1], (int)$at[2], (int)$at[0]); + $y = (int)$ad[0]; + $m = (int)$ad[1]; + $d = (int)$ad[2]; + $a['timestamp'] = mktime(0, 0, 0, $m, $d, $y); - if ($diff < -604800 && $ad[0] == $at[0]) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformatshort'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - elseif ($diff < -604800) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformat2'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - elseif ($diff < -86400) { $a['class'] = 'past'; $a['str'] = sprintf($lang->get('daysago'),ceil(abs($diff)/86400)); } - elseif ($diff < 0) { $a['class'] = 'past'; $a['str'] = $lang->get('yesterday'); } - elseif ($diff < 86400) { $a['class'] = 'today'; $a['str'] = $lang->get('today'); } - elseif ($diff < 172800) { $a['class'] = 'today'; $a['str'] = $lang->get('tomorrow'); } - elseif ($diff < 691200) { $a['class'] = 'soon'; $a['str'] = sprintf($lang->get('indays'),ceil($diff/86400)); } - elseif ($ad[0] == $at[0]) { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformatshort'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - else { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformat2'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } + $oToday = new DateTimeImmutable(date("Y-m-d")); + $oDue = new DateTimeImmutable($duedate); + $oDiff = $oToday->diff($oDue); + if ($oDiff === false) { + return $a; + } + $thisYear = ((int)$oToday->format('Y') == $y); + $days = $oDiff->days; + if ($oDiff->invert) $days *= -1; + + if ($days < -7 && !$thisYear) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformat2'), $y, $m, $d, $lang); } + elseif ($days < -7) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } + elseif ($days < -1) { $a['class'] = 'past'; $a['str'] = sprintf($lang->get('daysago'), abs($days)); } + elseif ($days == -1) { $a['class'] = 'past'; $a['str'] = $lang->get('yesterday'); } + elseif ($days == 0) { $a['class'] = 'today'; $a['str'] = $lang->get('today'); } + elseif ($days == 1) { $a['class'] = 'today'; $a['str'] = $lang->get('tomorrow'); } + elseif ($days <= 7) { $a['class'] = 'soon'; $a['str'] = sprintf($lang->get('indays'), $days); } + elseif ($thisYear) { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } + else { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformat2'), $y, $m, $d, $lang); } #avoid short year $fmt = str_replace('y', 'Y', Config::get('dateformat2')); @@ -726,14 +688,6 @@ class TasksController extends ApiController { return (int)$s; } - private function daysInMonth(int $m, int $y = 0): int - { - if ($y == 0) $y = (int)date('Y'); - $a = array(1=>31,(($y-2000)%4?28:29),31,30,31,30,31,31,30,31,30,31); - if (isset($a[$m])) return $a[$m]; - else return 0; - } - private function getTagId($tag) { $db = DBConnection::instance(); @@ -787,79 +741,6 @@ class TasksController extends ApiController { } } - private function parseSmartSyntax($title): array - { - $a = [ - 'prio' => 0, - 'title' => $title, - 'tags' => '', - 'duedate' => null, - ]; - // priority - if ( preg_match("|^([-+]{1}\d+)(.+)|", $a['title'], $m) ) { - $a['prio'] = (int) $m[1]; - if ( $a['prio'] < -1 ) $a['prio'] = -1; - elseif ( $a['prio'] > 2 ) $a['prio'] = 2; - $a['title'] = trim($m[2]); - } - // duedate - if ( preg_match("|(.+)@(\S+)$|", $a['title'], $m) ) { - $rest = $m[1]; - $duepre = $m[2]; - $duedate = $this->findDuedate($duepre); - if ($duedate) { - $a['duedate'] = $duedate; - $a['title'] = $rest; - } - } - // tags - $tags = []; - $a['title'] = trim( preg_replace_callback( - "/(?:^|\s+)#([^#\s]+)/", - function ($matches) use (&$tags) { - $tags[] = $matches[1]; - return ''; - }, - $a['title'] - ) ); - if (count($tags) > 0) { - $a['tags'] = implode( ',' , $tags ); - } - do_filter('parseSmartSyntax', $title, $a); - - return $a; - } - - private function findDuedate(string $s): ?string - { - $duedate = null; - if (preg_match("|(\d+)([dwmy]{1})|",$s, $m)) { // 5d,2w... - $count = (int)$m[1]; - $period = $m[2]; - if ($period == 'd' || $period == 'w') { // days, weeks - if ($period == 'w') $count *= 7; - $duedate = date("Y-m-d", time() + 86400*$count); - } - else if ($period == 'm' || $period == 'y') { //months,years - if ($period == 'y') $count *= 12; - $a = explode(',', date('Y,m,d')); - $y = (int)$a[0]; - $m = (int)$a[1] + $count; - $d = (int)$a[2]; - if ($m > 12) { - $yy = (int)floor($m/12); - $y += $yy; - $m = $m - $yy*12; - } - $d = min($d, $this->daysInMonth($m, $y)); - $duedate = "$y-$m-$d"; - } - } - else { - $duedate = $this->parseDuedate($s); - } - return $duedate; - } } diff --git a/src/includes/common.php b/src/includes/common.php index cc34e8e..e981316 100644 --- a/src/includes/common.php +++ b/src/includes/common.php @@ -80,6 +80,16 @@ function formatDate3($format, $ay, $am, $ad, $lang) return strtr($format, array('Y'=>$Y, 'y'=>$y, 'F'=>$F, 'M'=>$M, 'n'=>$n, 'm'=>$m, 'd'=>$d, 'j'=>$j)); } +function daysInMonth(int $m, int $y = 0): int +{ + if ($y == 0) $y = (int)date('Y'); + $isLeap = (0 == $y % 4) && ((0 != $y % 100) || (0 == $y % 400)); + $a = array(1=>31, ($isLeap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + if (isset($a[$m])) return $a[$m]; + else return 0; +} + + function getRequestUri() { // Do not use HTTP_X_REWRITE_URL due to CVE-2018-14773 diff --git a/src/includes/smartsyntax.php b/src/includes/smartsyntax.php new file mode 100644 index 0000000..bf8a820 --- /dev/null +++ b/src/includes/smartsyntax.php @@ -0,0 +1,196 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +class MTTSmartSyntax implements MTTSmartSyntaxInterface +{ + protected $tagPrefix = '#'; + protected $duedatePrefix = '@!'; + + /** @var MTTSmartSyntaxInterface */ + protected static $instance; + + public static function instance(): MTTSmartSyntaxInterface + { + if (!isset(static::$instance)) { + static::$instance = new static(); + } + return static::$instance; + } + + public function parse(string $title): array + { + $a = [ + 'prio' => 0, + 'title' => $title, + 'tags' => '', + 'duedate' => null, + ]; + // priority + if ( preg_match("|^([-+]{1}\d+)(.+)|", $a['title'], $m) ) { + $a['prio'] = (int) $m[1]; + if ( $a['prio'] < -1 ) $a['prio'] = -1; + elseif ( $a['prio'] > 2 ) $a['prio'] = 2; + $a['title'] = trim($m[2]); + } + // duedate + if ( preg_match("|(.+)[{$this->duedatePrefix}]{1}(\S+)$|", $a['title'], $m) ) { + $rest = $m[1]; + $duepre = $m[2]; + $duedate = $this->findDuedate($duepre); + if ($duedate) { + $a['duedate'] = $duedate; + $a['title'] = $rest; + } + } + // tags + $tags = []; + $a['title'] = trim( preg_replace_callback( + "/(?:^|\s+)[{$this->tagPrefix}]{1}([^{$this->tagPrefix}\s]+)/", + function ($matches) use (&$tags) { + $tags[] = $matches[1]; + return ''; + }, + $a['title'] + ) ); + if (count($tags) > 0) { + $a['tags'] = implode( ',' , $tags ); + } + return $a; + } + + private function findDuedate(string $s): ?string + { + if (preg_match("|^(\d+)([dwmy]{1})$|",$s, $m)) { // 5d,2w... + $count = (int)$m[1]; + $period = $m[2]; + if ($period == 'd' || $period == 'w') { // days, weeks + if ($period == 'w') $count *= 7; + return date("Y-m-d", time() + 86400*$count); + } + else if ($period == 'm' || $period == 'y') { //months,years + if ($period == 'y') $count *= 12; + $a = explode(',', date('Y,m,d')); + $y = (int)$a[0]; + $m = (int)$a[1] + $count; + $d = (int)$a[2]; + if ($m > 12) { + $yy = (int)floor($m/12); + $y += $yy; + $m = $m - $yy*12; + } + $d = min($d, daysInMonth($m, $y)); + return "$y-$m-$d"; + } + } + + if (null !== $duedate = $this->parseDuedate($s)) { + return $duedate; + } + + $lang = Lang::instance(); + //TODO: add 3-letter short? + $needle = mb_strtolower($s); + $wd = null; + foreach ($lang->get('days_min') as $idx => $weekday) { + if ($needle === mb_strtolower($weekday)) { + $wd = $idx; + break; + } + } + if (null === $wd) { + foreach ($lang->get('days_long') as $idx => $weekday) { + if ($needle === mb_strtolower($weekday)) { + $wd = $idx; + break; + } + } + } + if (null !== $wd) { + $curWD = (int)date('w'); + $daysAdd = 0; + if ($wd <= $curWD) { //next week + $daysAdd = 7 - ($curWD - $wd); + } + else { //current week + $daysAdd = $wd - $curWD; + } + $oDue = new DateTime(); + $oDue->add( new DateInterval("P{$daysAdd}D") ); + return $oDue->format('Y-m-d'); + } + + return null; + } + + /** + * Try to parse input string as a duedate and return in format "Y-m-d". + * Return null if fail. + * @param string $s + * @return null|string + */ + public static function parseDuedate(string $s): ?string + { + $df2 = Config::get('dateformat2'); + if (max((int)strpos($df2,'n'), (int)strpos($df2,'m')) > max((int)strpos($df2,'d'), (int)strpos($df2,'j'))) $formatDayFirst = true; + else $formatDayFirst = false; + + $y = $m = $d = 0; + if (preg_match("|^(\d+)-(\d+)-(\d+)\b|", $s, $ma)) { + $y = (int)$ma[1]; $m = (int)$ma[2]; $d = (int)$ma[3]; + } + elseif (preg_match("|^(\d+)\/(\d+)\/(\d+)\b|", $s, $ma)) + { + if($formatDayFirst) { + $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; + } else { + $m = (int)$ma[1]; $d = (int)$ma[2]; $y = (int)$ma[3]; + } + } + elseif (preg_match("|^(\d+)\.(\d+)\.(\d+)\b|", $s, $ma)) { + $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; + } + elseif (preg_match("|^(\d+)\.(\d+)\b|", $s, $ma)) { + $d = (int)$ma[1]; $m = (int)$ma[2]; + $a = explode(',', date('Y,m,d')); + if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; + else $y = (int)$a[0]; + } + elseif (preg_match("|^(\d+)\/(\d+)\b|", $s, $ma)) + { + if($formatDayFirst) { + $d = (int)$ma[1]; $m = (int)$ma[2]; + } else { + $m = (int)$ma[1]; $d = (int)$ma[2]; + } + $a = explode(',', date('Y,m,d')); + if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; + else $y = (int)$a[0]; + } + else return null; + if ($y < 100) $y = 2000 + $y; + elseif ($y < 1000 || $y > 2099) $y = 2000 + (int)substr((string)$y, -2); + if ($m > 12) $m = 12; + $maxdays = daysInMonth($m,$y); + if ($m < 10) $m = '0'.$m; + if ($d > $maxdays) $d = $maxdays; + elseif ($d < 10) $d = '0'.$d; + return "$y-$m-$d"; + } +} + +interface MTTSmartSyntaxInterface +{ + public function parse(string $title): array; +} + +function parseSmartSyntax(string $title): ?array +{ + $a = MTTSmartSyntax::instance()->parse($title); + do_filter('parseSmartSyntax', $title, $a); + return $a; +}