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