*/ declare(strict_types=1); use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\Logger; use dokuwiki\plugin\yearbox\services\pageNameStrategies\PageNameStrategy; /** * All DokuWiki plugins to extend the parser/rendering mechanism * need to inherit from this class */ class syntax_plugin_yearbox extends SyntaxPlugin { /** * What kind of syntax is this? */ public function getType() { return 'substition'; } public function getPType() { return 'block'; } /** * What modes are allowed within this mode? */ public function getAllowedTypes() { return ['substition', 'protected', 'disabled', 'formatting']; } /** * What position in the sort order? */ public function getSort() { return 125; } /** * Connect pattern to lexer */ public function connectTo($mode) { $this->Lexer->addSpecialPattern('{{yearbox>.*?}}', $mode, 'plugin_yearbox'); } /** * Handle the match * E.g.: {{yearbox>year=2010;name=journal;size=12;ns=diary}} * */ public function handle($match, $state, $pos, Doku_Handler $handler) { global $INFO; $opt = []; // default options $opt['ns'] = $INFO['namespace'] ?? ''; // this namespace $opt['size'] = 12; // 12px font size $opt['name'] = 'day'; // a boring default page name $opt['year'] = date('Y'); // this year $opt['recent'] = false; // special 1-2 row 'recent pages' view... $opt['months'] = []; // months to be displayed (csv list), e.g. 1,2,3,4... 1=Sun $opt['weekdays'] = []; // weekdays which should have links (csv links)... 1=Jan $opt['align'] = ''; // default is centred $optionsString = substr($match, 10, -2); $args = explode(';', $optionsString); foreach ($args as $arg) { [$key, $value] = explode('=', $arg); switch ($key) { case 'year': $opt['year'] = $value; break; case 'name': $opt['name'] = $value; break; case 'fontsize': case 'size': $opt['size'] = $value; break; case 'ns': $opt['ns'] = (strpos($value, ':') === false) ? ':' . $value : $value; break; case 'recent': $opt['recent'] = ((int)$value > 0) ? (int)$value : 0; break; case 'months': $opt['months'] = explode(',', $value); break; case 'weekdays': $opt['weekdays'] = explode(',', $value); break; case 'align': if (in_array($value, ['left', 'right'])) { $opt['align'] = $value; } break; default: if ( class_exists(Logger::class)) { Logger::getInstance(Logger::LOG_DEBUG)->log( "Unknown key: '$key' in '$match'" ); } else { // TODO: remove after the next DokuWiki release dbglog("Yearbox Plugin: Unknown key '$key' in '$match'"); } } } return $opt; } /** * Create output */ public function render($mode, Doku_Renderer $renderer, $opt) { if ($mode == 'xhtml') { $renderer->doc .= $this->buildCalendar($opt); return true; } return false; } /** * Builds a complete HTML calendar of the year given * Provides a link to a page for each day of the year, with a popup abstract of page content * * $opt = array( * * @param string $year build calendar for one year (2011), or range of years (2011,2013) * @param string $name prefix for new page name, e.g diary, journal, day * @param int $size font size to use * @param string $ns root namespace for new page names * @param int $recent previous days that must be visible * @param array $months which months are visible (1-12), 1=Jan, 2=Feb, etc * @param array $weekdays which weekdays should have links (1-7), 1=Sun, 2=Mon, etc... * } * * @return string Complete marked up calendar table */ private function buildCalendar($opt) { $day_names = $this->getLang('yearbox_days'); $cal = ''; [$years, $first_weekday, $table_cols, $today] = $this->defineCalendar($opt); end($years); $last_year = key($years); // initial CSS $font_css = ($opt['size'] != 0) ? ' style="font-size:' . $opt['size'] . 'px;"' : ''; if ($opt['align'] == 'left') { $align = ' class=left'; } elseif ($opt['align'] == 'right') { $align = ' class=right'; } else { $align = ''; } $cal .= '
'; foreach ($years as $year_num => $year) { // display the year and day-of-week header $cal .= ''; for ($col = 0; $col < $table_cols; $col++) { $weekday_num = ($col + $first_weekday) % 7; // current day of week as a number if ($col == 0) { $cal .= '' . $year_num . ''; } $h = $day_names[$weekday_num]; $cal .= '' . $h . ''; } $cal .= ''; foreach ($year as $mth_num => $month) { $cal .= $this->getMonthHTML( $month, $mth_num, $opt, $year_num, $table_cols, $first_weekday, $today ); } // separator between years in a range if ($year_num != $last_year) { $cal .= ''; } } $cal .= '
'; return $cal; } /** * Get the HTML for one table-row, representing one month * * @param $month * @param $mth_num * @param $opt * @param $year_num * @param $table_cols * @param $first_weekday * @param $today * * @return string */ protected function getMonthHTML( $month, $mth_num, $opt, $year_num, $table_cols, $first_weekday, $today ) { $cal = ''; // insert month name into first column of row $cal .= $this->getMonthNameHTML($mth_num); $cur_day = 0; for ($col = 0; $col < $table_cols; $col++) { $weekday_num = ($col + $first_weekday) % 7; // current day of week as a number // current day is only valid if within the month's days, and at the correct starting day if (($cur_day > 0 && $cur_day < $month['len']) || ($col < 7 && $weekday_num == $month['start'])) { $cur_day++; $cal .= $this->getDayHTML($cur_day, $mth_num, $today, $year_num, $weekday_num, $opt); } else { $cur_day = 0; $cal .= $this->getEmptyCellHTML(); } } $cal .= ''; return $cal; } /** * @param int $cur_day Day of the month * @param int $mth_num Month 1..12 * @param int $today ts today midnight FIXME * @param int $year_num year as YYYY * @param int $weekday_num day of the week 0..6 (0=sunday, 6=saturday) * @param array $opt config from handler * * @return string */ public function getDayHTML($cur_day, $mth_num, $today, $year_num, $weekday_num, $opt) { if (!$this->isWeekdayToBePrinted($weekday_num, $opt)) { return $this->getEmptyCellHTML(); } global $conf; $is_weekend = $weekday_num === 0 || $weekday_num === 6; $day_css = ($is_weekend) ? ' class="wkend"' : ''; $day_fmt = sprintf("%02d", $cur_day); $month_fmt = sprintf("%02d", $mth_num); $pagenameService = PageNameStrategy::getPagenameStategy($this->getConf('namestructure')); $id = $pagenameService->getPageId($opt['ns'], $year_num, $month_fmt, $day_fmt, $opt['name']); $current = mktime(0, 0, 0, $mth_num, $cur_day, $year_num); if ($current == $today) { $day_css = ' class="today"'; } $link = $this->getDayLinkHTML($id, $day_fmt, $conf[ 'userewrite' ]); return '' . $link . ''; } /** * Determine if the given weekday should be printed or be an empty cell * * @param $weekday_num * @param $opt * * @return bool */ protected function isWeekdayToBePrinted($weekday_num, $opt) { if (empty($opt['weekdays'])) { return true; } return in_array($weekday_num, $opt['weekdays']); } /** * Get the HTML for a header cell with the month name * * @param $mth_num * * @return string */ protected function getMonthNameHTML($mth_num) { $month_names = [ $this->getLang('yearbox_months_jan'), $this->getLang('yearbox_months_feb'), $this->getLang('yearbox_months_mar'), $this->getLang('yearbox_months_apr'), $this->getLang('yearbox_months_may'), $this->getLang('yearbox_months_jun'), $this->getLang('yearbox_months_jul'), $this->getLang('yearbox_months_aug'), $this->getLang('yearbox_months_sep'), $this->getLang('yearbox_months_oct'), $this->getLang('yearbox_months_nov'), $this->getLang('yearbox_months_dec'), ]; $alt_css = ($mth_num % 2 == 0) ? ' class="alt"' : ''; return '' . $month_names[$mth_num - 1] . ''; } /** * Get the HTML for an empty cell * * @return string */ protected function getEmptyCellHTML() { return '   '; } /** * establish list of valid months and days, ready for building the visible calendar * * @param array $opt users options */ private function defineCalendar($opt) { $years = []; $table_cols = 0; $first_weekday = 6; $year_range = explode(',', $opt['year']); $today = mktime(0, 0, 0, (int)date('m'), (int)date('d'), (int)date('Y')); // work out the date range first if ($opt['recent'] > 0) { // recent days (matching at least no. of recent days given; shows complete months only) $mth_last = (int)date('n'); $yr_last = (int)date('Y'); $prev_date = $today - ($opt['recent'] * 12 * 60 * 60); $mth_first = (int)date('n', $prev_date); $yr_first = (int)date('Y', $prev_date); $mth_last += ($yr_last - $yr_first) * 12; } elseif (count($year_range) == 2) { // if user provides two years: first -> last (inclusive) $mth_first = 1; [$yr_first, $yr_last] = $year_range; $mth_last = 12 + ($yr_last - $yr_first) * 12; } else { // plain old one year calender $mth_first = 1; $mth_last = 12; $yr_first = $yr_last = $opt['year']; } $show_all_mths = empty($opt['months']); // first get start day for each month, and length of month, // exact no. of columns needed, and the starting day of week for ($mth = $mth_first; $mth <= $mth_last; $mth++) { $mth_num = ($mth - 1) % 12 + 1; // real month number (1-12) // only consider displayed months when calculating column size if ($show_all_mths || in_array($mth_num, $opt['months'])) { $year = $yr_first + floor(($mth - 1) / 12); // allow for year overlaps $start = date('w', mktime(0, 0, 0, $mth_num, 1, (int)$year)); $len = date('j', mktime(0, 0, 0, $mth_num + 1, 0, (int)$year)); // save the first weekday (0-6; 0=Sun) and length (days) of this month $years[$year][$mth_num] = ['start' => $start, 'len' => $len]; // max number of table columns needed (not including col for months!) $table_cols = ($table_cols < ($start + $len)) ? $start + $len : $table_cols; // find the lowest day of week (i.e. Sun = 0, Mon = 1, etc...) // this determines which day of week to begin column headers with $first_weekday = ($first_weekday > $start) ? $start : $first_weekday; } } // final total columns needed in HTML table $table_cols -= $first_weekday; return [$years, $first_weekday, $table_cols, $today]; } private function wikilinkPreviewPopup($id, $name) { // swap normal link title (popup) for a more useful preview $link = html_wikilink($id, $name); $meta = p_get_metadata($id, false, true); $abstract = $meta['description']['abstract'] . '… ' . "\nEdited: " . date('Y-M-d', $meta['date']['modified']); $preview = htmlentities($abstract, ENT_QUOTES, 'UTF-8'); $link = preg_replace('/title=\".+?\"/', 'title="' . $preview . '"', $link, 1); return $link; } /** * @param string $id * @param string $day_fmt * @param $userewrite * * @return string|string[]|null */ private function getDayLinkHTML(string $id, string $day_fmt, $userewrite) { if (page_exists($id)) { return $this->wikilinkPreviewPopup($id, $day_fmt); } $link = html_wikilink($id, $day_fmt); // skip the "do you want to create this page" bit $sym = ($userewrite) ? '?' : '&'; return preg_replace('/\" class/', $sym . 'do=edit" class', $link, 1); } }