xref: /plugin/combo/ComboStrap/Call.php (revision 37748cd8654635afbeca80942126742f0f4cc346)
1<?php
2/**
3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15use dokuwiki\Extension\SyntaxPlugin;
16
17
18/**
19 * Class Call
20 * @package ComboStrap
21 *
22 * A wrapper around what's called a call
23 * which is an array of information such
24 * the mode, the data
25 *
26 * The {@link CallStack} is the only syntax representation that
27 * is available in DokuWiki
28 */
29class Call
30{
31
32    const INLINE_DISPLAY = "inline";
33    const BlOCK_DISPLAY = "block";
34    /**
35     * List of inline components
36     * Used to manage white space before an unmatched string.
37     * The syntax tree of Dokuwiki (ie {@link \Doku_Handler::$calls})
38     * has only data and no class, for now, we create this
39     * lists manually because this is a hassle to retrieve this information from {@link \DokuWiki_Syntax_Plugin::getType()}
40     */
41    const INLINE_DOKUWIKI_COMPONENTS = array(
42        /**
43         * Formatting https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
44         * Comes from the {@link \dokuwiki\Parsing\ParserMode\Formatting} class
45         */
46        "cdata",
47        "unformatted", // ie %% or nowiki
48        "doublequoteclosing", // https://www.dokuwiki.org/config:typography / https://www.dokuwiki.org/wiki:syntax#text_to_html_conversions
49        "doublequoteopening",
50        "singlequoteopening",
51        "singlequoteclosing",
52        "multiplyentity",
53        "apostrophe",
54        "strong",
55        "emphasis",
56        "emphasis_open",
57        "emphasis_close",
58        "underline",
59        "monospace",
60        "subscript",
61        "superscript",
62        "deleted",
63        "footnote",
64        /**
65         * Others
66         */
67        "acronym",
68        "strong_close",
69        "strong_open",
70        "monospace_open",
71        "monospace_close",
72        "doublequoteopening", // ie the character " in "The"
73        "entity", // for instance `...` are transformed in character
74        "linebreak",
75        "externallink",
76        "internallink",
77        MediaLink::INTERNAL_MEDIA_CALL_NAME,
78        MediaLink::EXTERNAL_MEDIA_CALL_NAME,
79        /**
80         * The inline of combo
81         * TODO: Should be deleted when {@link PluginUtility::renderUnmatched()} is not using the array anymore
82         * but is using {@link Call::getDisplay()} instead or any other rewrite
83         */
84        \syntax_plugin_combo_link::TAG,
85        \syntax_plugin_combo_icon::TAG,
86        \syntax_plugin_combo_inote::TAG,
87        \syntax_plugin_combo_button::TAG,
88        \syntax_plugin_combo_tooltip::TAG,
89        \syntax_plugin_combo_pipeline::TAG,
90    );
91
92
93    const BLOCK_MARKUP_DOKUWIKI_COMPONENTS = array(
94        "listu_open", // ul
95        "listu_close",
96        "listitem_open", //li
97        "listitem_close",
98        "listcontent_open", // after li ???
99        "listcontent_close",
100        "table_open",
101        "table_close",
102    );
103
104    private $call;
105
106    /**
107     * The key identifier in the {@link CallStack}
108     * @var mixed|string
109     */
110    private $key;
111
112    /**
113     * Call constructor.
114     * @param $call - the instruction array (ie called a call)
115     */
116    public function __construct(&$call, $key = "")
117    {
118        $this->call = &$call;
119        $this->key = $key;
120    }
121
122    /**
123     * Insert a tag above
124     * @param $tagName
125     * @param $state
126     * @param $attribute
127     * @param $context
128     * @param string $content
129     * @return Call - a call
130     */
131    public static function createComboCall($tagName, $state, $attribute = array(), $context = null, $content = '', $payload = null)
132    {
133        $data = array(
134            PluginUtility::ATTRIBUTES => $attribute,
135            PluginUtility::CONTEXT => $context,
136            PluginUtility::STATE => $state
137        );
138        if ($payload != null) {
139            $data[PluginUtility::PAYLOAD] = $payload;
140        }
141        $positionInText = null;
142
143        $call = [
144            "plugin",
145            array(
146                PluginUtility::getComponentName($tagName),
147                $data,
148                $state,
149                $content
150            ),
151            $positionInText
152        ];
153        return new Call($call);
154    }
155
156    /**
157     * Insert a dokuwiki call
158     * @param $callName
159     * @param $array
160     * @param $positionInText
161     * @return Call
162     */
163    public static function createNativeCall($callName, $array = [], $positionInText = null)
164    {
165        $call = [
166            $callName,
167            $array,
168            $positionInText
169        ];
170        return new Call($call);
171    }
172
173    public static function createFromInstruction($instruction)
174    {
175        return new Call($instruction);
176    }
177
178
179    /**
180     *
181     * Return the tag name from a call array
182     *
183     * This is not the logical tag.
184     * This is much more what's called:
185     *   * the component name for a plugin
186     *   * or the handler name for dokuwiki
187     *
188     * For a plugin, this is equivalent
189     * to the {@link SyntaxPlugin::getPluginComponent()}
190     *
191     * This is not the fully qualified component name:
192     *   * with the plugin as prefix such as in {@link Call::getComponentName()}
193     *   * or with the `open` and `close` prefix such as `p_close` ...
194     *
195     * @return mixed|string
196     */
197    public function getTagName()
198    {
199        $mode = $this->call[0];
200        if ($mode != "plugin") {
201
202            /**
203             * This is a standard dokuwiki node
204             */
205            $dokuWikiNodeName = $this->call[0];
206
207            /**
208             * The dokwuiki node name has also the open and close notion
209             * We delete this is not in the doc and therefore not logical
210             */
211            $tagName = str_replace("_close", "", $dokuWikiNodeName);
212            $tagName = str_replace("_open", "", $tagName);
213
214        } else {
215
216            /**
217             * This is a plugin node
218             */
219            $pluginDokuData = $this->call[1];
220            $component = $pluginDokuData[0];
221            if (!is_array($component)) {
222                /**
223                 * Tag name from class
224                 */
225                $componentNames = explode("_", $component);
226                /**
227                 * To take care of
228                 * PHP Warning:  sizeof(): Parameter must be an array or an object that implements Countable
229                 * in lib/plugins/combo/class/Tag.php on line 314
230                 */
231                if (is_array($componentNames)) {
232                    $tagName = $componentNames[sizeof($componentNames) - 1];
233                } else {
234                    $tagName = $component;
235                }
236            } else {
237                // To resolve: explode() expects parameter 2 to be string, array given
238                LogUtility::msg("The call (" . print_r($this->call, true) . ") has an array and not a string as component (" . print_r($component, true) . "). Page: " . PluginUtility::getPageId(), LogUtility::LVL_MSG_ERROR);
239                $tagName = "";
240            }
241
242
243        }
244        return $tagName;
245
246    }
247
248
249    /**
250     * The parser state
251     * @return mixed
252     * May be null (example eol, internallink, ...)
253     */
254    public function getState()
255    {
256        $mode = $this->call[0];
257        if ($mode != "plugin") {
258
259            /**
260             * There is no state because this is a standard
261             * dokuwiki syntax found in {@link \Doku_Renderer_xhtml}
262             * check if this is not a `...._close` or `...._open`
263             * to derive the state
264             */
265            $mode = $this->call[0];
266            $lastPositionSepName = strrpos($mode, "_");
267            $closeOrOpen = substr($mode, $lastPositionSepName + 1);
268            switch ($closeOrOpen) {
269                case "open":
270                    return DOKU_LEXER_ENTER;
271                case "close":
272                    return DOKU_LEXER_EXIT;
273                default:
274                    return null;
275            }
276
277        } else {
278            // Plugin
279            $returnedArray = $this->call[1];
280            if (array_key_exists(2, $returnedArray)) {
281                return $returnedArray[2];
282            } else {
283                return null;
284            }
285        }
286    }
287
288    /**
289     * @return mixed the data returned from the {@link DokuWiki_Syntax_Plugin::handle} (ie attributes, payload, ...)
290     */
291    public function &getPluginData()
292    {
293        return $this->call[1][1];
294    }
295
296    /**
297     * @return mixed the matched content from the {@link DokuWiki_Syntax_Plugin::handle}
298     */
299    public function getCapturedContent()
300    {
301        $caller = $this->call[0];
302        switch ($caller) {
303            case "plugin":
304                return $this->call[1][3];
305            case "internallink":
306                return '[[' . $this->call[1][0] . '|' . $this->call[1][1] . ']]';
307            case "eol":
308                return DOKU_LF;
309            case "header":
310            case "cdata":
311                return $this->call[1][0];
312            default:
313                if (isset($this->call[1][0]) && is_string($this->call[1][0])) {
314                    return $this->call[1][0];
315                } else {
316                    return "";
317                }
318        }
319    }
320
321
322    public function getAttributes()
323    {
324
325        $tagName = $this->getTagName();
326        switch ($tagName) {
327            case MediaLink::INTERNAL_MEDIA_CALL_NAME:
328                return $this->call[1];
329            default:
330                $data = $this->getPluginData();
331                if (isset($data[PluginUtility::ATTRIBUTES])) {
332                    return $data[PluginUtility::ATTRIBUTES];
333                } else {
334                    return null;
335                }
336        }
337    }
338
339    public function removeAttributes()
340    {
341
342        $data = &$this->getPluginData();
343        if (isset($data[PluginUtility::ATTRIBUTES])) {
344            unset($data[PluginUtility::ATTRIBUTES]);
345        }
346
347    }
348
349    public function updateToPluginComponent($component, $state, $attributes = array())
350    {
351        if ($this->call[0] == "plugin") {
352            $match = $this->call[1][3];
353        } else {
354            $this->call[0] = "plugin";
355            $match = "";
356        }
357        $this->call[1] = array(
358            0 => $component,
359            1 => array(
360                PluginUtility::ATTRIBUTES => $attributes,
361                PluginUtility::STATE => $state,
362            ),
363            2 => $state,
364            3 => $match
365        );
366
367    }
368
369    public function getDisplay()
370    {
371        if ($this->getState() == DOKU_LEXER_UNMATCHED) {
372            /**
373             * Unmatched are content (ie text node in XML/HTML) and have
374             * no display
375             */
376            return Call::INLINE_DISPLAY;
377        } else {
378            $mode = $this->call[0];
379            if ($mode == "plugin") {
380                global $DOKU_PLUGINS;
381                $component = $this->getComponentName();
382                /**
383                 * @var SyntaxPlugin $syntaxPlugin
384                 */
385                $syntaxPlugin = $DOKU_PLUGINS['syntax'][$component];
386                $pType = $syntaxPlugin->getPType();
387                switch ($pType) {
388                    case "normal":
389                        return Call::INLINE_DISPLAY;
390                    case "block":
391                    case "stack":
392                        return Call::BlOCK_DISPLAY;
393                    default:
394                        LogUtility::msg("The ptype (" . $pType . ") is unknown.");
395                        return null;
396                }
397            } else {
398                if ($mode == "eol") {
399                    /**
400                     * Control character
401                     * We return it as it's used in the
402                     * {@link \syntax_plugin_combo_para::fromEolToParagraphUntilEndOfStack()}
403                     * to create the paragraph
404                     * This is not a block, nor an inline
405                     */
406                    return $mode;
407                }
408
409                if (in_array($mode, self::INLINE_DOKUWIKI_COMPONENTS)) {
410                    return Call::INLINE_DISPLAY;
411                }
412
413                if (in_array($mode, self::BLOCK_MARKUP_DOKUWIKI_COMPONENTS)) {
414                    return Call::BlOCK_DISPLAY;
415                }
416
417                LogUtility::msg("The display of the call with the mode " . $mode . " is unknown");
418                return null;
419
420
421            }
422        }
423
424    }
425
426    /**
427     * Same as {@link Call::getTagName()}
428     * but fully qualified
429     * @return string
430     */
431    public function getComponentName()
432    {
433        $mode = $this->call[0];
434        if ($mode == "plugin") {
435            $pluginDokuData = $this->call[1];
436            return $pluginDokuData[0];
437        } else {
438            return $mode;
439        }
440    }
441
442    public function updateEolToSpace()
443    {
444        $mode = $this->call[0];
445        if ($mode != "eol") {
446            LogUtility::msg("You can't update a " . $mode . " to a space. It should be a eol", LogUtility::LVL_MSG_WARNING, "support");
447        } else {
448            $this->call[0] = "cdata";
449            $this->call[1] = array(
450                0 => " "
451            );
452        }
453
454    }
455
456    public function addAttribute($key, $value)
457    {
458        $mode = $this->call[0];
459        if ($mode == "plugin") {
460            $this->call[1][1][PluginUtility::ATTRIBUTES][$key] = $value;
461        } else {
462            LogUtility::msg("You can't add an attribute to the non plugin call mode (" . $mode . ")", LogUtility::LVL_MSG_WARNING, "support");
463        }
464    }
465
466    public function getContext()
467    {
468        $mode = $this->call[0];
469        if ($mode == "plugin") {
470            return $this->call[1][1][PluginUtility::CONTEXT];
471        } else {
472            LogUtility::msg("You can't ask for a context from a non plugin call mode (" . $mode . ")", LogUtility::LVL_MSG_WARNING, "support");
473            return null;
474        }
475    }
476
477    /**
478     *
479     * @return array
480     */
481    public function toCallArray()
482    {
483        return $this->call;
484    }
485
486    public function __toString()
487    {
488        $name = $this->key;
489        if (!empty($name)) {
490            $name .= " - ";
491        }
492        $name .= $this->getTagName();
493        return $name;
494    }
495
496    public function getType()
497    {
498        if ($this->getState() == DOKU_LEXER_UNMATCHED) {
499            LogUtility::msg("The unmatched tag ($this) does not have any attributes. Get its parent if you want the type", LogUtility::LVL_MSG_ERROR);
500            return null;
501        } else {
502            /**
503             * don't use {@link Call::getAttribute()} to get the type
504             * as this function stack also depends on
505             * this function {@link Call::getType()}
506             * to return the value
507             * Ie: if this is a boolean attribute without specified type
508             * if the boolean value is in the type, we return it
509             */
510            return $this->call[1][1][PluginUtility::ATTRIBUTES][TagAttributes::TYPE_KEY];
511        }
512    }
513
514    /**
515     * @param $key
516     * @param null $default
517     * @return string|null
518     */
519    public function getAttribute($key, $default = null)
520    {
521        $attributes = $this->getAttributes();
522        if (isset($attributes[$key])) {
523            return $attributes[$key];
524        } else {
525            // boolean attribute
526            if ($this->getType() == $key) {
527                return true;
528            } else {
529                return $default;
530            }
531        }
532    }
533
534    public function getPayload()
535    {
536        $mode = $this->call[0];
537        if ($mode == "plugin") {
538            return $this->call[1][1][PluginUtility::PAYLOAD];
539        } else {
540            LogUtility::msg("You can't ask for a payload from a non plugin call mode (" . $mode . ").", LogUtility::LVL_MSG_WARNING, "support");
541            return null;
542        }
543    }
544
545    public function setContext($value)
546    {
547        $this->call[1][1][PluginUtility::CONTEXT] = $value;
548        return $this;
549    }
550
551    public function hasAttribute($attributeName)
552    {
553        $attributes = $this->getAttributes();
554        if (isset($attributes[$attributeName])) {
555            return true;
556        } else {
557            if ($this->getType() == $attributeName) {
558                return true;
559            } else {
560                return false;
561            }
562        }
563    }
564
565    public function isPluginCall()
566    {
567        return $this->call[0] === "plugin";
568    }
569
570    /**
571     * @return mixed|string the position (ie key) in the array
572     */
573    public function getKey()
574    {
575        return $this->key;
576    }
577
578    public function &getCall()
579    {
580        return $this->call;
581    }
582
583    public function setState($state)
584    {
585        if ($this->call[0] == "plugin") {
586            // for dokuwiki
587            $this->call[1][2] = $state;
588            // for the combo plugin if any
589            if (isset($this->call[1][1][PluginUtility::STATE])) {
590                $this->call[1][1][PluginUtility::STATE] = $state;
591            }
592        } else {
593            LogUtility::msg("This modification of state is not yet supported for a native call");
594        }
595    }
596
597
598    /**
599     * Return the position of the first matched character in the text file
600     * @return mixed
601     */
602    public function getFirstMatchedCharacterPosition()
603    {
604
605        return $this->call[2];
606
607    }
608
609    /**
610     * Return the position of the last matched character in the text file
611     *
612     * This is the {@link Call::getFirstMatchedCharacterPosition()}
613     * plus the length of the {@link Call::getCapturedContent()}
614     * matched content
615     * @return int|mixed
616     */
617    public function getLastMatchedCharacterPosition()
618    {
619        return $this->getFirstMatchedCharacterPosition() + strlen($this->getCapturedContent());
620    }
621
622    /**
623     * @param $value string the class string to add
624     * @return Call
625     */
626    public function addClassName($value)
627    {
628        $class = $this->getAttribute("class");
629        if ($class != null) {
630            $value = "$class $value";
631        }
632        $this->addAttribute("class", $value);
633        return $this;
634
635    }
636
637    /**
638     * @param $key
639     * @return mixed|null - the delete value of null if not found
640     */
641    public function removeAttribute($key)
642    {
643
644        $data = &$this->getPluginData();
645        if (isset($data[PluginUtility::ATTRIBUTES][$key])) {
646            $value = $data[PluginUtility::ATTRIBUTES][$key];
647            unset($data[PluginUtility::ATTRIBUTES][$key]);
648            return $value;
649        } else {
650            // boolean attribute as first attribute
651            if ($this->getType() == $key) {
652                unset($data[PluginUtility::ATTRIBUTES][TagAttributes::TYPE_KEY]);
653                return true;
654            }
655            return null;
656        }
657
658    }
659
660    public function setPayload($text)
661    {
662        if ($this->isPluginCall()) {
663            $this->call[1][1][PluginUtility::PAYLOAD] = $text;
664        } else {
665            LogUtility::msg("Setting the payload for a non-native call ($this) is not yet implemented");
666        }
667    }
668
669    /**
670     * @return bool true if the call is a text call (same as dom text node)
671     */
672    public function isTextCall()
673    {
674        return (
675            $this->getState() == DOKU_LEXER_UNMATCHED ||
676            $this->getTagName() == "cdata" ||
677            $this->getTagName() == "acronym"
678        );
679    }
680
681    public function setType($type)
682    {
683        if ($this->isPluginCall()) {
684            $this->call[1][1][PluginUtility::ATTRIBUTES][TagAttributes::TYPE_KEY] = $type;
685        } else {
686            LogUtility::msg("This is not a plugin call ($this), you can't set the type");
687        }
688    }
689
690    public function addCssStyle($key, $value)
691    {
692        $style = $this->getAttribute("style");
693        $cssValue = "$key:$value";
694        if ($style != null) {
695            $cssValue = "$style; $cssValue";
696        }
697        $this->addAttribute("style", $cssValue);
698    }
699
700    public function setSyntaxComponentFromTag($tag)
701    {
702
703        if ($this->isPluginCall()) {
704            $this->call[1][0] = PluginUtility::getComponentName($tag);
705        } else {
706            LogUtility::msg("The call ($this) is a native call and we don't support yet the modification of the component to ($tag)");
707        }
708    }
709
710    /**
711     * @param Page $page
712     * @return Call
713     */
714    public function render(Page $page)
715    {
716        return $this->renderFromData(TemplateUtility::getMetadataDataFromPage($page));
717    }
718
719    public function renderFromData(array $array)
720    {
721        $state = $this->getState();
722        if ($state == DOKU_LEXER_UNMATCHED) {
723            if ($this->isPluginCall()) {
724                $payload = $this->getPayload();
725                if (!empty($payload)) {
726                    $this->setPayload(TemplateUtility::renderStringTemplateFromDataArray($payload, $array));
727                }
728            }
729        } else {
730            $tagName = $this->getTagName();
731            switch ($tagName) {
732                case "eol":
733                    break;
734                case "cdata":
735                    $payload = $this->getCapturedContent();
736                    $this->setCapturedContent(TemplateUtility::renderStringTemplateFromDataArray($payload, $array));
737                    break;
738                case \syntax_plugin_combo_pipeline::TAG:
739                    $pageTemplate = PluginUtility::getTagContent($this->getCapturedContent());
740                    $script = TemplateUtility::renderStringTemplateFromDataArray($pageTemplate, $array);
741                    $string = PipelineUtility::execute($script);
742                    $this->setPayload($string);
743                    break;
744                case \syntax_plugin_combo_link::TAG:
745                    switch ($this->getState()) {
746                        case DOKU_LEXER_ENTER:
747                            $ref = $this->getAttribute("ref");
748                            $this->addAttribute("ref", TemplateUtility::renderStringTemplateFromDataArray($ref, $array));
749                            break;
750                    }
751                    break;
752            }
753        }
754        return $this;
755    }
756
757    public function setCapturedContent($content)
758    {
759        $tagName = $this->getTagName();
760        switch ($tagName) {
761            case "cdata":
762                $this->call[1][0] = $content;
763                break;
764            default:
765                LogUtility::msg("Setting the captured content on a call for the tag ($tagName) is not yet implemented", LogUtility::LVL_MSG_ERROR);
766        }
767    }
768
769
770}
771