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