1<?php 2 3/** 4 * yearbox Plugin: provides a year calendar, with links to a new page for each day 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Symon Bent: <symonbent [at] gmail [dot] com> 8 */ 9 10declare(strict_types=1); 11 12use dokuwiki\Extension\SyntaxPlugin; 13use dokuwiki\Logger; 14use dokuwiki\plugin\yearbox\services\pageNameStrategies\PageNameStrategy; 15 16/** 17 * All DokuWiki plugins to extend the parser/rendering mechanism 18 * need to inherit from this class 19 */ 20class syntax_plugin_yearbox extends SyntaxPlugin 21{ 22 23 /** 24 * What kind of syntax is this? 25 */ 26 public function getType() 27 { 28 return 'substition'; 29 } 30 31 public function getPType() 32 { 33 return 'block'; 34 } 35 36 /** 37 * What modes are allowed within this mode? 38 */ 39 public function getAllowedTypes() 40 { 41 return ['substition', 'protected', 'disabled', 'formatting']; 42 } 43 44 /** 45 * What position in the sort order? 46 */ 47 public function getSort() 48 { 49 return 125; 50 } 51 52 /** 53 * Connect pattern to lexer 54 */ 55 public function connectTo($mode) 56 { 57 $this->Lexer->addSpecialPattern('{{yearbox>.*?}}', $mode, 'plugin_yearbox'); 58 } 59 60 /** 61 * Handle the match 62 * E.g.: {{yearbox>year=2010;name=journal;size=12;ns=diary}} 63 * 64 */ 65 public function handle($match, $state, $pos, Doku_Handler $handler) 66 { 67 global $INFO; 68 $opt = []; 69 70 // default options 71 $opt['ns'] = $INFO['namespace'] ?? ''; // this namespace 72 $opt['size'] = 12; // 12px font size 73 $opt['name'] = 'day'; // a boring default page name 74 $opt['year'] = date('Y'); // this year 75 $opt['recent'] = false; // special 1-2 row 'recent pages' view... 76 $opt['months'] = []; // months to be displayed (csv list), e.g. 1,2,3,4... 1=Sun 77 $opt['weekdays'] = []; // weekdays which should have links (csv links)... 1=Jan 78 $opt['align'] = ''; // default is centred 79 80 $optionsString = substr($match, 10, -2); 81 $args = explode(';', $optionsString); 82 foreach ($args as $arg) { 83 [$key, $value] = explode('=', $arg); 84 switch ($key) { 85 case 'year': 86 $opt['year'] = $value; 87 break; 88 case 'name': 89 $opt['name'] = $value; 90 break; 91 case 'fontsize': 92 case 'size': 93 $opt['size'] = $value; 94 break; 95 case 'ns': 96 $opt['ns'] = (strpos($value, ':') === false) ? ':' . $value : $value; 97 break; 98 case 'recent': 99 $opt['recent'] = ((int)$value > 0) ? (int)$value : 0; 100 break; 101 case 'months': 102 $opt['months'] = explode(',', $value); 103 break; 104 case 'weekdays': 105 $opt['weekdays'] = explode(',', $value); 106 break; 107 case 'align': 108 if (in_array($value, ['left', 'right'])) { 109 $opt['align'] = $value; 110 } 111 break; 112 default: 113 if ( class_exists(Logger::class)) { 114 Logger::getInstance(Logger::LOG_DEBUG)->log( 115 "Unknown key: '$key' in '$match'" 116 ); 117 } else { 118 // TODO: remove after the next DokuWiki release 119 dbglog("Yearbox Plugin: Unknown key '$key' in '$match'"); 120 } 121 } 122 } 123 return $opt; 124 } 125 126 /** 127 * Create output 128 */ 129 public function render($mode, Doku_Renderer $renderer, $opt) 130 { 131 if ($mode == 'xhtml') { 132 $renderer->doc .= $this->buildCalendar($opt); 133 return true; 134 } 135 return false; 136 } 137 138 139 /** 140 * Builds a complete HTML calendar of the year given 141 * Provides a link to a page for each day of the year, with a popup abstract of page content 142 * 143 * $opt = array( 144 * 145 * @param string $year build calendar for one year (2011), or range of years (2011,2013) 146 * @param string $name prefix for new page name, e.g diary, journal, day 147 * @param int $size font size to use 148 * @param string $ns root namespace for new page names 149 * @param int $recent previous days that must be visible 150 * @param array $months which months are visible (1-12), 1=Jan, 2=Feb, etc 151 * @param array $weekdays which weekdays should have links (1-7), 1=Sun, 2=Mon, etc... 152 * } 153 * 154 * @return string Complete marked up calendar table 155 */ 156 private function buildCalendar($opt) 157 { 158 $day_names = $this->getLang('yearbox_days'); 159 $cal = ''; 160 161 [$years, $first_weekday, $table_cols, $today] = $this->defineCalendar($opt); 162 end($years); 163 $last_year = key($years); 164 165 // initial CSS 166 $font_css = ($opt['size'] != 0) ? ' style="font-size:' . $opt['size'] . 'px;"' : ''; 167 if ($opt['align'] == 'left') { 168 $align = ' class=left'; 169 } elseif ($opt['align'] == 'right') { 170 $align = ' class=right'; 171 } else { 172 $align = ''; 173 } 174 $cal .= '<div class="yearbox"' . $font_css . '><table' . $align . '><tbody>'; 175 176 foreach ($years as $year_num => $year) { 177 // display the year and day-of-week header 178 $cal .= '<tr class="yr-header">'; 179 for ($col = 0; $col < $table_cols; $col++) { 180 $weekday_num = ($col + $first_weekday) % 7; // current day of week as a number 181 if ($col == 0) { 182 $cal .= '<th class="plain">' . $year_num . '</th>'; 183 } 184 $h = $day_names[$weekday_num]; 185 $cal .= '<th>' . $h . '</th>'; 186 } 187 $cal .= '</tr>'; 188 189 foreach ($year as $mth_num => $month) { 190 $cal .= $this->getMonthHTML( 191 $month, 192 $mth_num, 193 $opt, 194 $year_num, 195 $table_cols, 196 $first_weekday, 197 $today 198 ); 199 } 200 // separator between years in a range 201 if ($year_num != $last_year) { 202 $cal .= '<tr class="blank"><td></td></tr>'; 203 } 204 } 205 206 $cal .= '</tbody></table></div><div class="clearer"></div>'; 207 return $cal; 208 } 209 210 /** 211 * Get the HTML for one table-row, representing one month 212 * 213 * @param $month 214 * @param $mth_num 215 * @param $opt 216 * @param $year_num 217 * @param $table_cols 218 * @param $first_weekday 219 * @param $today 220 * 221 * @return string 222 */ 223 protected function getMonthHTML( 224 $month, 225 $mth_num, 226 $opt, 227 $year_num, 228 $table_cols, 229 $first_weekday, 230 $today 231 ) { 232 $cal = '<tr>'; 233 // insert month name into first column of row 234 $cal .= $this->getMonthNameHTML($mth_num); 235 $cur_day = 0; 236 for ($col = 0; $col < $table_cols; $col++) { 237 $weekday_num = ($col + $first_weekday) % 7; // current day of week as a number 238 239 // current day is only valid if within the month's days, and at the correct starting day 240 if (($cur_day > 0 && $cur_day < $month['len']) || ($col < 7 && $weekday_num == $month['start'])) { 241 $cur_day++; 242 $cal .= $this->getDayHTML($cur_day, $mth_num, $today, $year_num, $weekday_num, $opt); 243 } else { 244 $cur_day = 0; 245 $cal .= $this->getEmptyCellHTML(); 246 } 247 } 248 $cal .= '</tr>'; 249 250 return $cal; 251 } 252 253 /** 254 * @param int $cur_day Day of the month 255 * @param int $mth_num Month 1..12 256 * @param int $today ts today midnight FIXME 257 * @param int $year_num year as YYYY 258 * @param int $weekday_num day of the week 0..6 (0=sunday, 6=saturday) 259 * @param array $opt config from handler 260 * 261 * @return string 262 */ 263 public function getDayHTML($cur_day, $mth_num, $today, $year_num, $weekday_num, $opt) 264 { 265 if (!$this->isWeekdayToBePrinted($weekday_num, $opt)) { 266 return $this->getEmptyCellHTML(); 267 } 268 269 global $conf; 270 $is_weekend = $weekday_num === 0 || $weekday_num === 6; 271 $day_css = ($is_weekend) ? ' class="wkend"' : ''; 272 $day_fmt = sprintf("%02d", $cur_day); 273 $month_fmt = sprintf("%02d", $mth_num); 274 $pagenameService = PageNameStrategy::getPagenameStategy($this->getConf('namestructure')); 275 $id = $pagenameService->getPageId($opt['ns'], $year_num, $month_fmt, $day_fmt, $opt['name']); 276 $current = mktime(0, 0, 0, $mth_num, $cur_day, $year_num); 277 if ($current == $today) { 278 $day_css = ' class="today"'; 279 } 280 281 $link = $this->getDayLinkHTML($id, $day_fmt, $conf[ 'userewrite' ]); 282 return '<td' . $day_css . '>' . $link . '</td>'; 283 } 284 285 /** 286 * Determine if the given weekday should be printed or be an empty cell 287 * 288 * @param $weekday_num 289 * @param $opt 290 * 291 * @return bool 292 */ 293 protected function isWeekdayToBePrinted($weekday_num, $opt) 294 { 295 if (empty($opt['weekdays'])) { 296 return true; 297 } 298 return in_array($weekday_num, $opt['weekdays']); 299 } 300 301 /** 302 * Get the HTML for a header cell with the month name 303 * 304 * @param $mth_num 305 * 306 * @return string 307 */ 308 protected function getMonthNameHTML($mth_num) 309 { 310 $month_names = [ 311 $this->getLang('yearbox_months_jan'), 312 $this->getLang('yearbox_months_feb'), 313 $this->getLang('yearbox_months_mar'), 314 $this->getLang('yearbox_months_apr'), 315 $this->getLang('yearbox_months_may'), 316 $this->getLang('yearbox_months_jun'), 317 $this->getLang('yearbox_months_jul'), 318 $this->getLang('yearbox_months_aug'), 319 $this->getLang('yearbox_months_sep'), 320 $this->getLang('yearbox_months_oct'), 321 $this->getLang('yearbox_months_nov'), 322 $this->getLang('yearbox_months_dec'), 323 ]; 324 $alt_css = ($mth_num % 2 == 0) ? ' class="alt"' : ''; 325 return '<th' . $alt_css . '>' . $month_names[$mth_num - 1] . '</th>'; 326 } 327 328 /** 329 * Get the HTML for an empty cell 330 * 331 * @return string 332 */ 333 protected function getEmptyCellHTML() 334 { 335 return '<td class="blank"> </td>'; 336 } 337 338 339 /** 340 * establish list of valid months and days, ready for building the visible calendar 341 * 342 * @param array $opt users options 343 */ 344 private function defineCalendar($opt) 345 { 346 $years = []; 347 348 $table_cols = 0; 349 $first_weekday = 6; 350 351 $year_range = explode(',', $opt['year']); 352 $today = mktime(0, 0, 0, (int)date('m'), (int)date('d'), (int)date('Y')); 353 354 // work out the date range first 355 if ($opt['recent'] > 0) { 356 // recent days (matching at least no. of recent days given; shows complete months only) 357 $mth_last = (int)date('n'); 358 $yr_last = (int)date('Y'); 359 $prev_date = $today - ($opt['recent'] * 12 * 60 * 60); 360 $mth_first = (int)date('n', $prev_date); 361 $yr_first = (int)date('Y', $prev_date); 362 $mth_last += ($yr_last - $yr_first) * 12; 363 } elseif (count($year_range) == 2) { 364 // if user provides two years: first -> last (inclusive) 365 $mth_first = 1; 366 [$yr_first, $yr_last] = $year_range; 367 $mth_last = 12 + ($yr_last - $yr_first) * 12; 368 } else { 369 // plain old one year calender 370 $mth_first = 1; 371 $mth_last = 12; 372 $yr_first = $yr_last = $opt['year']; 373 } 374 $show_all_mths = empty($opt['months']); 375 376 // first get start day for each month, and length of month, 377 // exact no. of columns needed, and the starting day of week 378 for ($mth = $mth_first; $mth <= $mth_last; $mth++) { 379 $mth_num = ($mth - 1) % 12 + 1; // real month number (1-12) 380 381 // only consider displayed months when calculating column size 382 if ($show_all_mths || in_array($mth_num, $opt['months'])) { 383 $year = $yr_first + floor(($mth - 1) / 12); // allow for year overlaps 384 $start = date('w', mktime(0, 0, 0, $mth_num, 1, (int)$year)); 385 $len = date('j', mktime(0, 0, 0, $mth_num + 1, 0, (int)$year)); 386 387 // save the first weekday (0-6; 0=Sun) and length (days) of this month 388 $years[$year][$mth_num] = ['start' => $start, 'len' => $len]; 389 390 // max number of table columns needed (not including col for months!) 391 $table_cols = ($table_cols < ($start + $len)) ? $start + $len : $table_cols; 392 393 // find the lowest day of week (i.e. Sun = 0, Mon = 1, etc...) 394 // this determines which day of week to begin column headers with 395 $first_weekday = ($first_weekday > $start) ? $start : $first_weekday; 396 } 397 } 398 // final total columns needed in HTML table 399 $table_cols -= $first_weekday; 400 401 return [$years, $first_weekday, $table_cols, $today]; 402 } 403 404 private function wikilinkPreviewPopup($id, $name) 405 { 406 // swap normal link title (popup) for a more useful preview 407 $link = html_wikilink($id, $name); 408 $meta = p_get_metadata($id, false, true); 409 $abstract = $meta['description']['abstract'] . '… ' . "\nEdited: " . date('Y-M-d', $meta['date']['modified']); 410 $preview = htmlentities($abstract, ENT_QUOTES, 'UTF-8'); 411 $link = preg_replace('/title=\".+?\"/', 'title="' . $preview . '"', $link, 1); 412 return $link; 413 } 414 415 /** 416 * @param string $id 417 * @param string $day_fmt 418 * @param $userewrite 419 * 420 * @return string|string[]|null 421 */ 422 private function getDayLinkHTML(string $id, string $day_fmt, $userewrite) 423 { 424 if (page_exists($id)) { 425 return $this->wikilinkPreviewPopup($id, $day_fmt); 426 } 427 428 $link = html_wikilink($id, $day_fmt); 429 // skip the "do you want to create this page" bit 430 $sym = ($userewrite) ? '?' : '&'; 431 return preg_replace('/\" class/', $sym . 'do=edit" class', $link, 1); 432 } 433} 434