1<?php 2 3 4use ComboStrap\CacheManager; 5use ComboStrap\CacheDependencies; 6use ComboStrap\Call; 7use ComboStrap\CallStack; 8use ComboStrap\ExceptionCombo; 9use ComboStrap\LogUtility; 10use ComboStrap\Page; 11use ComboStrap\PageImages; 12use ComboStrap\PageSql; 13use ComboStrap\PageSqlTreeListener; 14use ComboStrap\PluginUtility; 15use ComboStrap\Sqlite; 16use ComboStrap\TagAttributes; 17use ComboStrap\Template; 18use ComboStrap\TemplateUtility; 19 20require_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 21 22 23/** 24 * 25 * An iterator to iterate over templates. 26 * 27 * ******************* 28 * Iteration driver 29 * ******************* 30 * The end tag of the template node is driving the iteration. 31 * This way, the tags just after the template 32 * sees them in the {@link CallStack} and can change their context 33 * 34 * For instance, a {@link syntax_plugin_combo_masonry} 35 * component will change the context of all card inside it. 36 * 37 * ******************** 38 * Header and footer delimitation 39 * ******************** 40 * The iterator delimits also the header and footer. 41 * Some component needs the header to be generate completely. 42 * This is the case of a complex markup such as a table 43 * 44 * ****************************** 45 * Delete if no data 46 * ****************************** 47 * It gives also the possibility to {@link syntax_plugin_combo_iterator::EMPTY_ROWS_COUNT_ATTRIBUTE 48 * delete the whole block} 49 * (header and footer also) if there is no data 50 * 51 * ***************************** 52 * Always Contextual 53 * ***************************** 54 * We don't capture the text markup such as in a {@link syntax_plugin_combo_code} 55 * in order to loop because you can't pass the actual handler (ie callstack) 56 * when you {@link p_get_instructions() parse again} a markup. 57 * 58 * The markup is then seen as a new single page without any context. 59 * That may lead to problems. 60 * Example: `heading` may then think that they are `outline heading` ... 61 * 62 */ 63class syntax_plugin_combo_iterator extends DokuWiki_Syntax_Plugin 64{ 65 66 /** 67 * Tag in Dokuwiki cannot have a `-` 68 * This is the last part of the class 69 */ 70 const TAG = "iterator"; 71 72 /** 73 * Page canonical and tag pattern 74 */ 75 const CANONICAL = "iterator"; 76 const PAGE_SQL = "page-sql"; 77 const VARIABLE_NAMES = "variable-names"; 78 const COMPLEX_MARKUP_FOUND = "complex-markup-found"; 79 const BEFORE_TEMPLATE_CALLSTACK = "header-callstack"; 80 const AFTER_TEMPLATE_CALLSTACK = "footer-callstack"; 81 const TEMPLATE_CALLSTACK = "template-callstack"; 82 83 84 /** 85 * Syntax Type. 86 * 87 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 88 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 89 * @see DokuWiki_Syntax_Plugin::getType() 90 */ 91 function getType(): string 92 { 93 return 'container'; 94 } 95 96 /** 97 * How Dokuwiki will add P element 98 * 99 * * 'normal' - The plugin can be used inside paragraphs (inline or inside) 100 * * 'block' - Open paragraphs need to be closed before plugin output (box) - block should not be inside paragraphs 101 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 102 * 103 * @see DokuWiki_Syntax_Plugin::getPType() 104 * @see https://www.dokuwiki.org/devel:syntax_plugins#ptype 105 */ 106 function getPType(): string 107 { 108 return 'block'; 109 } 110 111 /** 112 * @return array 113 * Allow which kind of plugin inside 114 * 115 * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 116 * because we manage self the content and we call self the parser 117 * 118 * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 119 */ 120 function getAllowedTypes(): array 121 { 122 return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 123 } 124 125 function getSort(): int 126 { 127 return 201; 128 } 129 130 public function accepts($mode): bool 131 { 132 return syntax_plugin_combo_preformatted::disablePreformatted($mode); 133 } 134 135 136 function connectTo($mode) 137 { 138 139 140 $pattern = PluginUtility::getContainerTagPattern(self::TAG); 141 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 142 143 144 } 145 146 147 public function postConnect() 148 { 149 150 $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent())); 151 152 153 } 154 155 156 /** 157 * 158 * The handle function goal is to parse the matched syntax through the pattern function 159 * and to return the result for use in the renderer 160 * This result is always cached until the page is modified. 161 * @param string $match 162 * @param int $state 163 * @param int $pos - byte position in the original source file 164 * @param Doku_Handler $handler 165 * @return array 166 * @see DokuWiki_Syntax_Plugin::handle() 167 * 168 */ 169 function handle($match, $state, $pos, Doku_Handler $handler): array 170 { 171 172 switch ($state) { 173 174 case DOKU_LEXER_ENTER : 175 176 $tagAttributes = TagAttributes::createFromTagMatch($match); 177 $callStackArray = $tagAttributes->toCallStackArray(); 178 return array( 179 PluginUtility::STATE => $state, 180 PluginUtility::ATTRIBUTES => $callStackArray 181 ); 182 183 case DOKU_LEXER_UNMATCHED : 184 185 // We should not ever come here but a user does not not known that 186 return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 187 188 189 case DOKU_LEXER_EXIT : 190 191 $callStack = CallStack::createFromHandler($handler); 192 $openTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 193 /** 194 * Scanning the callstack and extracting the information 195 * such as sql and template instructions 196 */ 197 $pageSql = null; 198 $beforeTemplateCallStack = []; 199 $templateStack = []; 200 $afterTemplateCallStack = []; 201 $parsingState = "before"; 202 $complexMarkupFound = false; 203 $variableNames = []; 204 while ($actualCall = $callStack->next()) { 205 $tagName = $actualCall->getTagName(); 206 switch ($tagName) { 207 case syntax_plugin_combo_iteratordata::TAG: 208 if ($actualCall->getState() === DOKU_LEXER_UNMATCHED) { 209 $pageSql = $actualCall->getCapturedContent(); 210 } 211 continue 2; 212 case syntax_plugin_combo_template::TAG: 213 $parsingState = "after"; 214 if ($actualCall->getState() === DOKU_LEXER_EXIT) { 215 $templateStack = $actualCall->getPluginData(syntax_plugin_combo_template::CALLSTACK); 216 /** 217 * Do we have markup where the instructions should be generated at once 218 * and not line by line 219 * 220 * ie a list or a table 221 */ 222 foreach ($templateStack as $templateInstructions) { 223 $templateCall = Call::createFromInstruction($templateInstructions); 224 if (in_array($templateCall->getComponentName(), Call::BLOCK_MARKUP_DOKUWIKI_COMPONENTS)) { 225 $complexMarkupFound = true; 226 } 227 228 /** 229 * Capture variable names 230 * to be able to find their value 231 * in the metadata if they are not in sql 232 */ 233 $textWithVariables = $templateCall->getCapturedContent(); 234 $attributes = $templateCall->getAttributes(); 235 if ($attributes !== null) { 236 $sep = " "; 237 foreach ($attributes as $key => $attribute) { 238 $textWithVariables .= $sep . $key . $sep . $attribute; 239 } 240 } 241 242 if (!empty($textWithVariables)) { 243 $template = Template::create($textWithVariables); 244 $variablesDetected = $template->getVariablesDetected(); 245 $variableNames = array_merge($variableNames, $variablesDetected); 246 } 247 } 248 } 249 continue 2; 250 default: 251 if ($parsingState === "before") { 252 $beforeTemplateCallStack[] = $actualCall->toCallArray(); 253 } else { 254 $afterTemplateCallStack[] = $actualCall->toCallArray(); 255 }; 256 break; 257 } 258 } 259 $variableNames = array_unique($variableNames); 260 261 /** 262 * Wipe the content of iterator 263 */ 264 $callStack->deleteAllCallsAfter($openTag); 265 266 return array( 267 PluginUtility::STATE => $state, 268 self::PAGE_SQL => $pageSql, 269 self::VARIABLE_NAMES => $variableNames, 270 self::COMPLEX_MARKUP_FOUND => $complexMarkupFound, 271 self::BEFORE_TEMPLATE_CALLSTACK => $beforeTemplateCallStack, 272 self::AFTER_TEMPLATE_CALLSTACK => $afterTemplateCallStack, 273 self::TEMPLATE_CALLSTACK => $templateStack 274 ); 275 276 } 277 return array(); 278 279 } 280 281 /** 282 * Render the output 283 * @param string $format 284 * @param Doku_Renderer $renderer 285 * @param array $data - what the function handle() return'ed 286 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 287 * @see DokuWiki_Syntax_Plugin::render() 288 * 289 * 290 */ 291 function render($format, Doku_Renderer $renderer, $data): bool 292 { 293 if ($format === "xhtml") { 294 $state = $data[PluginUtility::STATE]; 295 switch ($state) { 296 case DOKU_LEXER_ENTER: 297 return true; 298 case DOKU_LEXER_UNMATCHED: 299 $renderer->doc .= PluginUtility::renderUnmatched($data); 300 return true; 301 case DOKU_LEXER_EXIT: 302 303 $pageSql = $data[self::PAGE_SQL]; 304 305 /** 306 * Data Processing 307 */ 308 if ($pageSql === null) { 309 $renderer->doc .= "A data node could not be found as a child of the iterator."; 310 return false; 311 } 312 if (empty($pageSql)) { 313 $renderer->doc .= "The data node definition needs a logical sql content"; 314 return false; 315 } 316 317 /** 318 * Sqlite available ? 319 */ 320 $sqlite = Sqlite::createOrGetSqlite(); 321 if ($sqlite === null) { 322 $renderer->doc .= "The iterator component needs Sqlite to be able to work"; 323 return false; 324 } 325 326 327 /** 328 * Create the SQL 329 */ 330 try { 331 $pageSql = PageSql::create($pageSql); 332 } catch (Exception $e) { 333 $renderer->doc .= "The page sql is not valid. Error Message: {$e->getMessage()}. Page Sql: ($pageSql)"; 334 return false; 335 } 336 337 $table = $pageSql->getTable(); 338 $cacheManager = CacheManager::getOrCreate(); 339 switch ($table) { 340 case PageSqlTreeListener::BACKLINKS: 341 $cacheManager->addDependencyForCurrentSlot(CacheDependencies::BACKLINKS_DEPENDENCY); 342 break; 343 default: 344 } 345 346 /** 347 * Execute the generated SQL 348 */ 349 try { 350 $executableSql = $pageSql->getExecutableSql(); 351 $parameters = $pageSql->getParameters(); 352 $request = $sqlite 353 ->createRequest() 354 ->setQueryParametrized($executableSql, $parameters); 355 $rowsInDb = []; 356 try { 357 $rowsInDb = $request 358 ->execute() 359 ->getRows(); 360 } catch (ExceptionCombo $e) { 361 $renderer->doc .= "The sql statement generated returns an error. Sql statement: $executableSql"; 362 return false; 363 } finally { 364 $request->close(); 365 } 366 367 $variableNames = $data[self::VARIABLE_NAMES]; 368 $rows = []; 369 foreach ($rowsInDb as $sourceRow) { 370 $analytics = $sourceRow["ANALYTICS"]; 371 /** 372 * @deprecated 373 * We use id until path is full in the database 374 */ 375 $id = $sourceRow["ID"]; 376 $page = Page::createPageFromId($id); 377 if ($page->isHidden()) { 378 continue; 379 } 380 $standardMetadata = $page->getMetadataForRendering(); 381 382 $jsonArray = json_decode($analytics, true); 383 $targetRow = []; 384 foreach ($variableNames as $variableName) { 385 386 if ($variableName === PageImages::PROPERTY_NAME) { 387 LogUtility::msg("To add an image, you must use the page image component, not the image metadata", LogUtility::LVL_MSG_ERROR, syntax_plugin_combo_pageimage::CANONICAL); 388 continue; 389 } 390 391 /** 392 * Data in the pages tables 393 */ 394 if (isset($sourceRow[strtoupper($variableName)])) { 395 $variableValue = $sourceRow[strtoupper($variableName)]; 396 $targetRow[$variableName] = $variableValue; 397 continue; 398 } 399 400 /** 401 * In the analytics 402 */ 403 $value = $jsonArray["metadata"][$variableName]; 404 if (!empty($value)) { 405 $targetRow[$variableName] = $value; 406 continue; 407 } 408 409 /** 410 * Computed 411 * (if the table is empty because of migration) 412 */ 413 $value = $standardMetadata[$variableName]; 414 if (isset($value)) { 415 $targetRow[$variableName] = $value; 416 continue; 417 } 418 419 /** 420 * Bad luck 421 */ 422 $targetRow[$variableName] = "$variableName attribute is unknown."; 423 424 425 } 426 $rows[] = $targetRow; 427 } 428 } catch (Exception $e) { 429 $renderer->doc .= "Error during Sql Execution. Error: {$e->getMessage()}"; 430 return false; 431 } 432 433 434 /** 435 * Loop 436 */ 437 $elementCounts = sizeof($rows); 438 if ($elementCounts === 0) { 439 $parametersString = implode(", ", $parameters); 440 LogUtility::msg("The physical query (Sql: {$pageSql->getExecutableSql()}, Parameters: $parametersString) does not return any data", LogUtility::LVL_MSG_INFO, syntax_plugin_combo_iterator::CANONICAL); 441 return true; 442 } 443 444 445 /** 446 * Template stack processing 447 */ 448 $iteratorTemplateInstructions = $data[self::TEMPLATE_CALLSTACK]; 449 if ($iteratorTemplateInstructions === null) { 450 $renderer->doc .= "No template was found in this iterator."; 451 return false; 452 } 453 $iteratorHeaderInstructions = $data[self::BEFORE_TEMPLATE_CALLSTACK]; 454 455 456 $iteratorTemplateGeneratedInstructions = []; 457 458 459 /** 460 * List and table syntax in template ? 461 */ 462 $complexMarkupFound = $data[self::COMPLEX_MARKUP_FOUND]; 463 if ($complexMarkupFound) { 464 465 /** 466 * Splits the template into header, main and footer 467 * @var Call $actualCall 468 */ 469 $templateCallStack = CallStack::createFromInstructions($iteratorTemplateInstructions); 470 $templateHeader = array(); 471 $templateMain = array(); 472 $actualStack = array(); 473 $templateCallStack->moveToStart(); 474 while ($actualCall = $templateCallStack->next()) { 475 switch ($actualCall->getComponentName()) { 476 case "listitem_open": 477 case "tablerow_open": 478 $templateHeader = $actualStack; 479 $actualStack = [$actualCall]; 480 continue 2; 481 case "listitem_close": 482 case "tablerow_close": 483 $actualStack[] = $actualCall; 484 $templateMain = $actualStack; 485 $actualStack = []; 486 continue 2; 487 default: 488 $actualStack[] = $actualCall; 489 } 490 } 491 $templateFooter = $actualStack; 492 493 /** 494 * Table with an header 495 * If this is the case, the table_close of the header 496 * and the table_open of the template should be 497 * deleted to create one table 498 */ 499 if (!empty($templateHeader)) { 500 $firstTemplateCall = $templateHeader[0]; 501 if ($firstTemplateCall->getComponentName() === "table_open") { 502 $lastIterationHeaderElement = sizeof($iteratorHeaderInstructions) - 1; 503 $lastIterationHeaderInstruction = Call::createFromInstruction($iteratorHeaderInstructions[$lastIterationHeaderElement]); 504 if ($lastIterationHeaderInstruction->getComponentName() === "table_close") { 505 unset($iteratorHeaderInstructions[$lastIterationHeaderElement]); 506 unset($templateHeader[0]); 507 } 508 } 509 } 510 511 /** 512 * Loop and recreate the call stack in instructions form for rendering 513 */ 514 $iteratorTemplateGeneratedInstructions = []; 515 foreach ($templateHeader as $templateHeaderCall) { 516 $iteratorTemplateGeneratedInstructions[] = $templateHeaderCall->toCallArray(); 517 } 518 foreach ($rows as $row) { 519 $templateInstructionForInstance = TemplateUtility::renderInstructionsTemplateFromDataArray($templateMain, $row); 520 $iteratorTemplateGeneratedInstructions = array_merge($iteratorTemplateGeneratedInstructions, $templateInstructionForInstance); 521 } 522 foreach ($templateFooter as $templateFooterCall) { 523 $iteratorTemplateGeneratedInstructions[] = $templateFooterCall->toCallArray(); 524 } 525 526 527 } else { 528 529 /** 530 * No Complex Markup 531 * We can use the calls form 532 */ 533 534 535 /** 536 * Append the new instructions by row 537 */ 538 foreach ($rows as $row) { 539 $templateInstructionForInstance = TemplateUtility::renderInstructionsTemplateFromDataArray($iteratorTemplateInstructions, $row); 540 $iteratorTemplateGeneratedInstructions = array_merge($iteratorTemplateGeneratedInstructions, $templateInstructionForInstance); 541 } 542 543 544 } 545 /** 546 * Rendering 547 */ 548 $totalInstructions = []; 549 // header 550 if (!empty($iteratorHeaderInstructions)) { 551 $totalInstructions = $iteratorHeaderInstructions; 552 } 553 // content 554 if (!empty($iteratorTemplateGeneratedInstructions)) { 555 $totalInstructions = array_merge($totalInstructions, $iteratorTemplateGeneratedInstructions); 556 } 557 // footer 558 $callStackFooterInstructions = $data[self::AFTER_TEMPLATE_CALLSTACK]; 559 if (!empty($callStackFooterInstructions)) { 560 $totalInstructions = array_merge($totalInstructions, $callStackFooterInstructions); 561 } 562 if (!empty($totalInstructions)) { 563 564 /** 565 * Advertise the total count to the 566 * {@link syntax_plugin_combo_carrousel} 567 * for the bullets if any 568 */ 569 $totalCallStack = CallStack::createFromInstructions($totalInstructions); 570 $totalCallStack->moveToEnd(); 571 while ($actualCall = $totalCallStack->previous()) { 572 if ( 573 $actualCall->getTagName() === syntax_plugin_combo_carrousel::TAG 574 && in_array($actualCall->getState(), [DOKU_LEXER_ENTER, DOKU_LEXER_EXIT]) 575 ) { 576 $actualCall->setPluginData(syntax_plugin_combo_carrousel::ELEMENT_COUNT, $elementCounts); 577 if ($actualCall->getState() === DOKU_LEXER_ENTER) { 578 break; 579 } 580 } 581 } 582 583 try { 584 $renderer->doc .= PluginUtility::renderInstructionsToXhtml($totalCallStack->getStack()); 585 } catch (ExceptionCombo $e) { 586 $renderer->doc .= "Error while rendering the iterators instructions. Error: {$e->getMessage()}"; 587 } 588 } 589 return true; 590 } 591 } 592 // unsupported $mode 593 return false; 594 } 595 596 597} 598 599