1<?php
2
3
4use ComboStrap\AnalyticsDocument;
5use ComboStrap\Call;
6use ComboStrap\CallStack;
7use ComboStrap\Canonical;
8use ComboStrap\DokuPath;
9use ComboStrap\ExceptionCombo;
10use ComboStrap\PageCreationDate;
11use ComboStrap\Metadata;
12use ComboStrap\PageImages;
13use ComboStrap\ResourceName;
14use ComboStrap\PagePath;
15use ComboStrap\PageSql;
16use ComboStrap\LogUtility;
17use ComboStrap\Page;
18use ComboStrap\Path;
19use ComboStrap\PluginUtility;
20use ComboStrap\PagePublicationDate;
21use ComboStrap\Sqlite;
22use ComboStrap\Template;
23use ComboStrap\TemplateUtility;
24
25
26require_once(__DIR__ . "/../ComboStrap/PluginUtility.php");
27
28/**
29 *
30 * Template
31 *
32 * A template capture the string
33 * and does not let the parser create the instructions.
34 *
35 * Why ?
36 * Because when you create a list with an {@link syntax_plugin_combo_iterator}
37 * A single list item such as
38 * `
39 *   * list
40 * `
41 * would be parsed as a complete list
42 *
43 * We create then the markup and we parse it.
44 *
45 */
46class syntax_plugin_combo_template extends DokuWiki_Syntax_Plugin
47{
48
49
50    const TAG = "template";
51
52    const ATTRIBUTES_IN_PAGE_TABLE = [
53        "id",
54        Canonical::PROPERTY_NAME,
55        PagePath::PROPERTY_NAME,
56        ModificationDate::PROPERTY_NAME,
57        PageCreationDate::PROPERTY_NAME,
58        PagePublicationDate::PROPERTY_NAME,
59        ResourceName::PROPERTY_NAME
60    ];
61
62    const CANONICAL = "template";
63
64    /**
65     * @param Call $call
66     */
67    public static function getCapturedTemplateContent($call)
68    {
69        $content = $call->getCapturedContent();
70        if (!empty($content)) {
71            if ($content[0] === DOKU_LF) {
72                $content = substr($content, 1);
73            }
74            /**
75             * To allow the template to be indented
76             * without triggering a {@link syntax_plugin_combo_preformatted}
77             */
78            $content = rtrim($content, " ");
79        }
80        return $content;
81    }
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()
92    {
93        return 'formatting';
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()
107    {
108        /**
109         * No P please
110         */
111        return 'normal';
112    }
113
114    /**
115     * @return array
116     * Allow which kind of plugin inside
117     *
118     * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
119     * because we manage self the content and we call self the parser
120     *
121     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
122     */
123    function getAllowedTypes()
124    {
125
126        return array('baseonly', 'container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
127    }
128
129    function getSort()
130    {
131        return 201;
132    }
133
134    public function accepts($mode)
135    {
136        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
137    }
138
139
140    function connectTo($mode)
141    {
142
143        $pattern = PluginUtility::getContainerTagPattern(self::TAG);
144        $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
145
146
147    }
148
149
150    public function postConnect()
151    {
152
153        $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent()));
154
155
156    }
157
158
159    /**
160     *
161     * The handle function goal is to parse the matched syntax through the pattern function
162     * and to return the result for use in the renderer
163     * This result is always cached until the page is modified.
164     * @param string $match
165     * @param int $state
166     * @param int $pos - byte position in the original source file
167     * @param Doku_Handler $handler
168     * @return array|bool
169     * @throws Exception
170     * @see DokuWiki_Syntax_Plugin::handle()
171     *
172     */
173    function handle($match, $state, $pos, Doku_Handler $handler)
174    {
175
176        switch ($state) {
177
178            case DOKU_LEXER_ENTER :
179
180                $attributes = PluginUtility::getTagAttributes($match);
181                return array(
182                    PluginUtility::STATE => $state,
183                    PluginUtility::ATTRIBUTES => $attributes
184                );
185
186            case DOKU_LEXER_UNMATCHED :
187
188                // We should not ever come here but a user does not not known that
189                return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
190
191
192            case DOKU_LEXER_EXIT :
193
194                $callStack = CallStack::createFromHandler($handler);
195
196                /**
197                 * Iterator node parent ?
198                 * The iterator is often the grand-parent
199                 * (ie a parent is generally a layout component
200                 * such as masonry, ...)
201                 */
202                $iteratorNode = null;
203                $callStack->moveToPreviousCorrespondingOpeningCall();
204                while ($parent = $callStack->moveToParent()) {
205                    if ($parent->getTagName() === syntax_plugin_combo_iterator::TAG) {
206                        $iteratorNode = $parent;
207                        break; // to start from the iterator
208                    }
209                }
210
211
212                /**
213                 * The array returned if any error
214                 */
215                $returnedArray = array(
216                    PluginUtility::STATE => $state
217                );
218
219                if ($iteratorNode === null) {
220
221                    /**
222                     * Gather template string
223                     */
224                    $callStack->moveToEnd();
225                    $templateEnterCall = $callStack->moveToPreviousCorrespondingOpeningCall();
226                    $templateStack = [];
227                    while ($actualCall = $callStack->next()) {
228                        $templateStack[] = $actualCall;
229                    }
230                    $callStack->deleteAllCallsAfter($templateEnterCall);
231
232                    /**
233                     * Template
234                     */
235                    $page = Page::createPageFromRequestedPage();
236                    $metadata = $page->getMetadataForRendering();
237                    $instructionsInstance = TemplateUtility::renderInstructionsTemplateFromDataArray($templateStack, $metadata);
238                    $callStack->appendInstructionsFromNativeArray($instructionsInstance);
239
240
241                } else {
242
243                    /**
244                     * Scanning the callstack and extracting the information
245                     * such as sql and template instructions
246                     */
247                    $pageSql = null;
248                    /**
249                     * @var Call[]
250                     */
251                    $actualStack = [];
252                    $complexMarkupFound = false;
253                    $variableNames = [];
254                    while ($actualCall = $callStack->next()) {
255
256                        /**
257                         * Capture Variable Names
258                         */
259                        $textWithVariables = $actualCall->getCapturedContent();
260                        $attributes = $actualCall->getAttributes();
261                        if ($attributes != null) {
262                            $sep = " ";
263                            foreach ($attributes as $key => $attribute) {
264                                $textWithVariables .= $sep . $key . $sep . $attribute;
265                            }
266                        }
267
268                        if (!empty($textWithVariables)) {
269                            $template = Template::create($textWithVariables);
270                            $variablesDetected = $template->getVariablesDetected();
271                            $variableNames = array_merge($variableNames, $variablesDetected);
272                        }
273
274                        /**
275                         * Other capture
276                         */
277                        switch ($actualCall->getTagName()) {
278                            case syntax_plugin_combo_iteratordata::TAG:
279                                if ($actualCall->getState() === DOKU_LEXER_UNMATCHED) {
280                                    $pageSql = $actualCall->getCapturedContent();
281                                }
282                                break;
283                            case self::TAG:
284                                if ($actualCall->getState() === DOKU_LEXER_ENTER) {
285                                    $headerStack = $actualStack;
286                                    $actualStack = [];
287                                } else {
288                                    $actualStack[] = $actualCall;
289                                }
290                                break;
291                            default:
292                                $actualStack[] = $actualCall;
293                                /**
294                                 * Do we have markup where the instructions should be generated at once
295                                 * and not line by line
296                                 *
297                                 * ie a list or a table
298                                 */
299                                if (in_array($actualCall->getComponentName(), Call::BLOCK_MARKUP_DOKUWIKI_COMPONENTS)) {
300                                    $complexMarkupFound = true;
301                                }
302
303                        }
304                    }
305                    $templateStack = $actualStack;
306                    $variableNames = array_unique($variableNames);
307
308
309                    /**
310                     * Data Processing
311                     */
312                    if ($pageSql === null) {
313                        LogUtility::msg("A data node could not be found in the iterator", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
314                        return $returnedArray;
315                    }
316                    if (empty($pageSql)) {
317                        LogUtility::msg("The data node definition needs a logical sql content", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
318                        return $returnedArray;
319                    }
320
321                    /**
322                     * Sqlite available ?
323                     */
324                    $sqlite = Sqlite::createOrGetSqlite();
325                    if ($sqlite === null) {
326                        LogUtility::msg("The iterator component needs Sqlite to be able to work", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
327                        return $returnedArray;
328                    }
329
330
331                    /**
332                     * Create the SQL
333                     */
334                    try {
335                        $pageSql = PageSql::create($pageSql);
336                    } catch (Exception $e) {
337                        LogUtility::msg("The page sql is not valid. Error Message: {$e->getMessage()}. Page Sql: ($pageSql)", LogUtility::LVL_MSG_ERROR, PageSql::CANONICAL);
338                        return $returnedArray;
339                    }
340
341                    /**
342                     * Execute the generated SQL
343                     */
344                    try {
345                        $executableSql = $pageSql->getExecutableSql();
346                        $parameters = $pageSql->getParameters();
347                        $request = $sqlite
348                            ->createRequest()
349                            ->setQueryParametrized($executableSql, $parameters);
350                        $rowsInDb = [];
351                        try{
352                            $rowsInDb = $request
353                                ->execute()
354                                ->getRows();
355                        } catch (ExceptionCombo $e){
356                            LogUtility::msg("The sql statement generated returns an error. Sql statement: $executableSql", LogUtility::LVL_MSG_ERROR);
357                        } finally {
358                            $request->close();
359                        }
360
361                        $rows = [];
362                        foreach ($rowsInDb as $sourceRow) {
363                            $analytics = $sourceRow["ANALYTICS"];
364                            /**
365                             * @deprecated
366                             * We use id until path is full in the database
367                             */
368                            $id = $sourceRow["ID"];
369                            $page = Page::createPageFromId($id);
370                            $standardMetadata = $page->getMetadataForRendering();
371
372                            $jsonArray = json_decode($analytics, true);
373                            $targetRow = [];
374                            foreach ($variableNames as $variableName) {
375
376                                if ($variableName === PageImages::PROPERTY_NAME) {
377                                    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);
378                                    continue;
379                                }
380
381                                /**
382                                 * Data in the pages tables
383                                 */
384                                if (isset($sourceRow[strtoupper($variableName)])) {
385                                    $data = $sourceRow[strtoupper($variableName)];
386                                    $targetRow[$variableName] = $data;
387                                    continue;
388                                }
389
390                                /**
391                                 * In the analytics
392                                 */
393                                $value = $jsonArray["metadata"][$variableName];
394                                if (!empty($value)) {
395                                    $targetRow[$variableName] = $value;
396                                    continue;
397                                }
398
399                                /**
400                                 * Computed
401                                 * (if the table is empty because of migration)
402                                 */
403                                $value = $standardMetadata[$variableName];
404                                if (isset($value)) {
405                                    $targetRow[$variableName] = $value;
406                                    continue;
407                                }
408
409                                /**
410                                 * Bad luck
411                                 */
412                                $targetRow[$variableName] = "$variableName attribute is unknown.";
413
414
415                            }
416                            $rows[] = $targetRow;
417                        }
418                    } catch (Exception $e) {
419                        LogUtility::msg($e->getMessage(), LogUtility::LVL_MSG_ERROR, self::CANONICAL);
420                        return $returnedArray;
421                    }
422
423
424                    /**
425                     * Loop
426                     */
427                    if (sizeof($rows) == 0) {
428                        $iteratorNode->addAttribute(syntax_plugin_combo_iterator::EMPTY_ROWS_COUNT_ATTRIBUTE, true);
429                        LogUtility::msg("The physical query ({$pageSql->getExecutableSql()}) does not return any data", LogUtility::LVL_MSG_INFO, syntax_plugin_combo_iterator::CANONICAL);
430                        return $returnedArray;
431                    }
432
433                    /**
434                     * List and table
435                     */
436                    if ($complexMarkupFound) {
437
438                        /**
439                         * Splits the template into header, main and footer
440                         * @var Call $actualCall
441                         */
442                        $templateHeader = array();
443                        $templateMain = array();
444                        $actualStack = array();
445                        foreach ($templateStack as $actualCall) {
446                            switch ($actualCall->getComponentName()) {
447                                case "listitem_open":
448                                case "tablerow_open":
449                                    $templateHeader = $actualStack;
450                                    $actualStack = [$actualCall];
451                                    continue 2;
452                                case "listitem_close":
453                                case "tablerow_close":
454                                    $actualStack[] = $actualCall;
455                                    $templateMain = $actualStack;
456                                    $actualStack = [];
457                                    continue 2;
458                                default:
459                                    $actualStack[] = $actualCall;
460                            }
461                        }
462                        $templateFooter = $actualStack;
463
464                        /**
465                         * Delete the template calls
466                         */
467                        $callStack->moveToEnd();;
468                        $openingTemplateCall = $callStack->moveToPreviousCorrespondingOpeningCall();
469                        $callStack->deleteAllCallsAfter($openingTemplateCall);
470
471                        /**
472                         * Table with an header
473                         * If this is the case, the table_close of the header
474                         * and the table_open of the template should be
475                         * deleted to create one table
476                         */
477                        if (!empty($templateHeader)) {
478                            $firstTemplateCall = $templateHeader[0];
479                            if ($firstTemplateCall->getComponentName() === "table_open") {
480                                $callStack->moveToEnd();
481                                $callStack->moveToPreviousCorrespondingOpeningCall();
482                                $previousCall = $callStack->previous();
483                                if ($previousCall->getComponentName() === "table_close") {
484                                    $callStack->deleteActualCallAndPrevious();
485                                    unset($templateHeader[0]);
486                                }
487                            }
488                        }
489                        /**
490                         * Loop and recreate the call stack
491                         */
492                        $callStack->appendInstructionsFromCallObjects($templateHeader);
493                        foreach ($rows as $row) {
494                            $instructionsInstance = TemplateUtility::renderInstructionsTemplateFromDataArray($templateMain, $row);
495                            $callStack->appendInstructionsFromNativeArray($instructionsInstance);
496                        }
497                        $callStack->appendInstructionsFromCallObjects($templateFooter);
498
499
500                    } else {
501
502                        /**
503                         * No Complex Markup
504                         * We can use the calls form
505                         */
506
507                        /**
508                         * Delete the template
509                         */
510                        $callStack->moveToEnd();
511                        $templateEnterCall = $callStack->moveToPreviousCorrespondingOpeningCall();
512                        $callStack->deleteAllCallsAfter($templateEnterCall);
513
514                        /**
515                         * Append the new instructions by row
516                         */
517                        foreach ($rows as $row) {
518                            $instructionsInstance = TemplateUtility::renderInstructionsTemplateFromDataArray($templateStack, $row);
519                            $callStack->appendInstructionsFromNativeArray($instructionsInstance);
520                        }
521
522                    }
523
524                }
525                return $returnedArray;
526
527
528        }
529        return array();
530
531    }
532
533    /**
534     * Render the output
535     * @param string $format
536     * @param Doku_Renderer $renderer
537     * @param array $data - what the function handle() return'ed
538     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
539     * @see DokuWiki_Syntax_Plugin::render()
540     *
541     *
542     */
543    function render($format, Doku_Renderer $renderer, $data)
544    {
545
546        if ($format === "xhtml") {
547            $state = $data[PluginUtility::STATE];
548            if ($state === DOKU_LEXER_UNMATCHED) {
549                $renderer->doc .= PluginUtility::renderUnmatched($data);
550            }
551        }
552        return false;
553
554    }
555
556
557}
558
559