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