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