1<?php
2/**
3 * DokuWiki Syntax Plugin Combostrap.
4 * Implementation of https://getbootstrap.com/docs/5.0/content/typography/#blockquotes
5 *
6 */
7
8namespace ComboStrap;
9
10
11use ComboStrap\Tag\BoxTag;
12use Doku_Handler;
13use Doku_Renderer_xhtml;
14use syntax_plugin_combo_cite;
15use syntax_plugin_combo_header;
16use syntax_plugin_combo_link;
17
18
19/**
20 * All DokuWiki plugins to extend the parser/rendering mechanism
21 * need to inherit from this class
22 *
23 * The name of the class must follow a pattern (don't change it)
24 * ie:
25 *    syntax_plugin_PluginName_ComponentName
26 */
27class BlockquoteTag
28{
29
30    const TAG = "blockquote";
31
32    /**
33     * When the blockquote is a tweet
34     */
35    const TWEET = "tweet";
36    const TWEET_SUPPORTED_LANG = array("en", "ar", "bn", "cs", "da", "de", "el", "es", "fa", "fi", "fil", "fr", "he", "hi", "hu", "id", "it", "ja", "ko", "msa", "nl", "no", "pl", "pt", "ro", "ru", "sv", "th", "tr", "uk", "ur", "vi", "zh-cn", "zh-tw");
37    const CONF_TWEET_WIDGETS_THEME = "twitter:widgets:theme";
38    const CONF_TWEET_WIDGETS_THEME_DEFAULT = "light";
39    const CONF_TWEET_WIDGETS_BORDER = "twitter:widgets:border-color";
40    const TYPO_TYPE = "typo";
41    const CARD_TYPE = "card";
42    const CONF_TWEET_WIDGETS_BORDER_DEFAULT = "#55acee";
43
44
45    /**
46     * @var mixed|string
47     */
48    static public $type = self::CARD_TYPE;
49
50
51    static function handleEnter($handler): array
52    {
53        /**
54         * Parent
55         */
56        $callStack = CallStack::createFromHandler($handler);
57        $context = null;
58        $parent = $callStack->moveToParent();
59        if ($parent !== false) {
60            $context = $parent->getTagName();
61            if ($context === FragmentTag::FRAGMENT_TAG) {
62                $parent = $callStack->moveToParent();
63                if ($parent !== false) {
64                    $context = $parent->getTagName();
65                }
66            }
67        }
68
69        return array(PluginUtility::CONTEXT => $context);
70
71    }
72
73
74    static public function handleExit(Doku_Handler $handler): array
75    {
76        $callStack = CallStack::createFromHandler($handler);
77
78        /**
79         * Check and add a scroll toggle if the
80         * blockquote is constrained by height
81         */
82        Dimension::addScrollToggleOnClickIfNoControl($callStack);
83
84
85        /**
86         * Pre-parsing:
87         *    Cite: A cite should be wrapped into a footer
88         *          This should happens before the p processing because we
89         *          are adding a {@link BoxTag} which is a stack
90         *    Tweet blockquote: If a link has tweet link status, this is a tweet blockquote
91         */
92        $callStack->moveToEnd();
93        $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
94        $tweetUrlFound = false;
95        while ($actualCall = $callStack->next()) {
96            if ($actualCall->getTagName() == syntax_plugin_combo_cite::TAG) {
97                switch ($actualCall->getState()) {
98                    case DOKU_LEXER_ENTER:
99                        // insert before
100                        $callStack->insertBefore(Call::createComboCall(
101                            BoxTag::TAG,
102                            DOKU_LEXER_ENTER,
103                            array(
104                                "class" => "blockquote-footer",
105                                BoxTag::HTML_TAG_ATTRIBUTE => "footer"
106                            ),
107                            null,
108                            null,
109                            null,
110                            null,
111                            \syntax_plugin_combo_xmlblocktag::TAG
112                        ));
113                        break;
114                    case DOKU_LEXER_EXIT:
115                        // insert after
116                        $callStack->insertAfter(Call::createComboCall(
117                            BoxTag::TAG,
118                            DOKU_LEXER_EXIT,
119                            array(
120                                BoxTag::HTML_TAG_ATTRIBUTE => "footer"
121                            ),
122                            null,
123                            null,
124                            null,
125                            null,
126                            \syntax_plugin_combo_xmlblocktag::TAG
127                        ));
128                        break;
129                }
130            }
131            if (
132                $actualCall->getTagName() == syntax_plugin_combo_link::TAG
133                && $actualCall->getState() == DOKU_LEXER_ENTER
134            ) {
135                $ref = $actualCall->getAttribute(syntax_plugin_combo_link::MARKUP_REF_ATTRIBUTE);
136                if (StringUtility::match($ref, "https:\/\/twitter.com\/[^\/]*\/status\/.*")) {
137                    $tweetUrlFound = true;
138                }
139            }
140        }
141        if ($tweetUrlFound) {
142            $context = BlockquoteTag::TWEET;
143            $type = $context;
144            $openingTag->setType($context);
145            $openingTag->setContext($context);
146        }
147
148        /**
149         * Because we can change the type above to tweet
150         * we set them after
151         */
152        $type = $openingTag->getType();
153        $context = $openingTag->getContext();
154        if ($context === null) {
155            $context = $type;
156        }
157        $attributes = $openingTag->getAttributes();
158
159        /**
160         * Create the paragraph
161         */
162        $callStack->moveToPreviousCorrespondingOpeningCall();
163        $callStack->insertEolIfNextCallIsNotEolOrBlock(); // eol is mandatory to have a paragraph if there is only content
164        $paragraphAttributes["class"] = "blockquote-text";
165        if ($type == self::TYPO_TYPE) {
166            $bootstrapVersion = Bootstrap::getBootStrapMajorVersion();
167            if ($bootstrapVersion == Bootstrap::BootStrapFourMajorVersion) {
168                // As seen here https://getbootstrap.com/docs/4.0/content/typography/#blockquotes
169                $paragraphAttributes["class"] .= " mb-0";
170                // not on 5 https://getbootstrap.com/docs/5.0/content/typography/#blockquotes
171            }
172        }
173        $callStack->processEolToEndStack($paragraphAttributes);
174
175        /**
176         * Wrap the blockquote into a card
177         *
178         * In a blockquote card, a blockquote typo is wrapped around a card
179         *
180         * We add then:
181         *   * at the body location: a card body start and a blockquote typo start
182         *   * at the end location: a card end body and a blockquote end typo
183         */
184        if ($type == self::CARD_TYPE) {
185
186            $callStack->moveToPreviousCorrespondingOpeningCall();
187            $callEnterTypeCall = Call::createComboCall(
188                self::TAG,
189                DOKU_LEXER_ENTER,
190                array(TagAttributes::TYPE_KEY => self::TYPO_TYPE),
191                $context,
192                null,
193                null,
194                null,
195                \syntax_plugin_combo_xmlblocktag::TAG
196            );
197            $cardBodyEnterCall = CardTag::createCardBodyEnterCall($context);
198            $firstChild = $callStack->moveToFirstChildTag();
199
200            if ($firstChild !== false) {
201                if ($firstChild->getTagName() == syntax_plugin_combo_header::TAG) {
202                    $callStack->moveToNextSiblingTag();
203                }
204                // Head: Insert card body
205                $callStack->insertBefore($cardBodyEnterCall);
206                // Head: Insert Blockquote typo
207                $callStack->insertBefore($callEnterTypeCall);
208
209            } else {
210                // No child
211                // Move back
212                $callStack->moveToEnd();;
213                $callStack->moveToPreviousCorrespondingOpeningCall();
214                // Head: Insert Blockquote typo
215                $callStack->insertAfter($callEnterTypeCall);
216                // Head: Insert card body
217                $callStack->insertAfter($cardBodyEnterCall);
218            }
219
220
221            /**
222             * End
223             */
224            // Insert the card body exit
225            $callStack->moveToEnd();
226            $callStack->insertBefore(
227                Call::createComboCall(
228                    self::TAG,
229                    DOKU_LEXER_EXIT,
230                    array("type" => self::TYPO_TYPE),
231                    $context,
232                    null,
233                    null,
234                    null,
235                    \syntax_plugin_combo_xmlblocktag::TAG
236                )
237            );
238            $callStack->insertBefore(CardTag::createCardBodyExitCall());
239        }
240
241        return array(
242            PluginUtility::CONTEXT => $context,
243            PluginUtility::ATTRIBUTES => $attributes
244        );
245    }
246
247    public static function renderEnterXhtml(TagAttributes $tagAttributes, $data, $renderer): string
248    {
249        /**
250         * Add the CSS
251         */
252        $snippetManager = PluginUtility::getSnippetManager();
253        $snippetManager->attachCssInternalStyleSheet(self::TAG);
254
255        /**
256         * Create the HTML
257         */
258        $type = $tagAttributes->getType();
259        switch ($type) {
260            case self::TYPO_TYPE:
261
262                $tagAttributes->addClassName("blockquote");
263                $cardTags = [CardTag::CARD_TAG, MasonryTag::MASONRY_TAG];
264                if (in_array($data[PluginUtility::CONTEXT], $cardTags)) {
265                    // As seen here: https://getbootstrap.com/docs/5.0/components/card/#header-and-footer
266                    // A blockquote in a card
267                    // This context is added dynamically when the blockquote is a card type
268                    $tagAttributes->addClassName("mb-0");
269                }
270                return $tagAttributes->toHtmlEnterTag("blockquote");
271
272            case self::TWEET:
273
274                try {
275                    PluginUtility::getSnippetManager()
276                        ->attachRemoteJavascriptLibrary(self::TWEET, "https://platform.twitter.com/widgets.js")
277                        ->addHtmlAttribute("id", "twitter-wjs");
278                } catch (ExceptionBadArgument|ExceptionBadSyntax $e) {
279                    LogUtility::internalError("It should not happen as the url is written by ons (ie is a literal)", self::TAG, $e);
280                }
281
282                $tagAttributes->addClassName("twitter-tweet");
283
284                $tweetAttributesNames = ["cards", "dnt", "conversation", "align", "width", "theme", "lang"];
285                foreach ($tweetAttributesNames as $tweetAttributesName) {
286                    if ($tagAttributes->hasComponentAttribute($tweetAttributesName)) {
287                        $value = $tagAttributes->getValueAndRemove($tweetAttributesName);
288                        $tagAttributes->addOutputAttributeValue("data-" . $tweetAttributesName, $value);
289                    }
290                }
291
292                return $tagAttributes->toHtmlEnterTag("blockquote");
293
294            case self::CARD_TYPE:
295            default:
296
297                /**
298                 * Wrap with column
299                 */
300                $context = $data[PluginUtility::CONTEXT];
301                if ($context === MasonryTag::MASONRY_TAG) {
302                    MasonryTag::addColIfBootstrap5AndCardColumns($renderer, $context);
303                }
304
305                /**
306                 * Starting the card
307                 */
308                $tagAttributes->addClassName(self::CARD_TYPE);
309                return $tagAttributes->toHtmlEnterTag("div") . DOKU_LF;
310            /**
311             * The card body and blockquote body
312             * of the example (https://getbootstrap.com/docs/4.0/components/card/#header-and-footer)
313             * are added via call at
314             * the {@link DOKU_LEXER_EXIT} state of {@link BlockquoteTag::handle()}
315             */
316        }
317    }
318
319
320    static function renderExitXhtml(TagAttributes $tagAttributes, Doku_Renderer_xhtml $renderer, array $data)
321    {
322        // Because we can have several unmatched on a line we don't know if
323        // there is a eol
324        StringUtility::addEolCharacterIfNotPresent($renderer->doc);
325        $type = $tagAttributes->getValue(TagAttributes::TYPE_KEY);
326        switch ($type) {
327            case self::CARD_TYPE:
328                $renderer->doc .= "</div>";
329                break;
330            case self::TWEET:
331            case self::TYPO_TYPE:
332            default:
333                $renderer->doc .= "</blockquote>";
334                break;
335        }
336
337        /**
338         * Closing the masonry column
339         * (Only if this is a card blockquote)
340         */
341        if ($type == CardTag::CARD_TAG) {
342            $context = $data[PluginUtility::CONTEXT];
343            if ($context === MasonryTag::MASONRY_TAG) {
344                MasonryTag::endColIfBootstrap5AnCardColumns($renderer, $context);
345            }
346        }
347
348    }
349
350
351}
352