xref: /plugin/combo/syntax/iterator.php (revision 4cadd4f8c541149bdda95f080e38a6d4e3a640ca)
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 in 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