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