xref: /plugin/combo/ComboStrap/OutlineVisitor.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1*04fd306cSNickeau<?php
2*04fd306cSNickeau
3*04fd306cSNickeaunamespace ComboStrap;
4*04fd306cSNickeau
5*04fd306cSNickeauuse ComboStrap\Meta\Field\FeaturedRasterImage;
6*04fd306cSNickeauuse ComboStrap\Meta\Field\FeaturedSvgImage;
7*04fd306cSNickeauuse ComboStrap\Tag\AdTag;
8*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
9*04fd306cSNickeau
10*04fd306cSNickeauclass OutlineVisitor
11*04fd306cSNickeau{
12*04fd306cSNickeau
13*04fd306cSNickeau
14*04fd306cSNickeau    private Outline $outline;
15*04fd306cSNickeau    private ?MarkupPath $markupPath;
16*04fd306cSNickeau    private int $currentLineCountSinceLastAd;
17*04fd306cSNickeau    /**
18*04fd306cSNickeau     * @var int the order, number of sections
19*04fd306cSNickeau     */
20*04fd306cSNickeau    private int $sectionNumbers;
21*04fd306cSNickeau    /**
22*04fd306cSNickeau     * @var int the number of ads inserted
23*04fd306cSNickeau     */
24*04fd306cSNickeau    private int $adsCounter;
25*04fd306cSNickeau    /**
26*04fd306cSNickeau     * @var OutlineSection The last section to be printed
27*04fd306cSNickeau     */
28*04fd306cSNickeau    private OutlineSection $lastSectionToBePrinted;
29*04fd306cSNickeau    private bool $inArticleEnabled;
30*04fd306cSNickeau
31*04fd306cSNickeau    public function __construct(Outline $outline)
32*04fd306cSNickeau    {
33*04fd306cSNickeau        $this->outline = $outline;
34*04fd306cSNickeau        $this->markupPath = $outline->getMarkupPath();
35*04fd306cSNickeau
36*04fd306cSNickeau        $this->inArticleEnabled = ExecutionContext::getActualOrCreateFromEnv()
37*04fd306cSNickeau            ->getConfig()
38*04fd306cSNickeau            ->getBooleanValue(AdTag::CONF_IN_ARTICLE_ENABLED, AdTag::CONF_IN_ARTICLE_ENABLED_DEFAULT);
39*04fd306cSNickeau        // Running variables that permits to balance the creation of Ads
40*04fd306cSNickeau        $this->currentLineCountSinceLastAd = 0;
41*04fd306cSNickeau        $this->sectionNumbers = 0;
42*04fd306cSNickeau        $this->adsCounter = 0;
43*04fd306cSNickeau        $this->lastSectionToBePrinted = $this->getLastSectionToBePrinted();
44*04fd306cSNickeau
45*04fd306cSNickeau    }
46*04fd306cSNickeau
47*04fd306cSNickeau    public static function create(Outline $outline): OutlineVisitor
48*04fd306cSNickeau    {
49*04fd306cSNickeau        return new OutlineVisitor($outline);
50*04fd306cSNickeau    }
51*04fd306cSNickeau
52*04fd306cSNickeau    public function getCalls()
53*04fd306cSNickeau    {
54*04fd306cSNickeau
55*04fd306cSNickeau        $totalCalls = [];
56*04fd306cSNickeau        $sectionSequenceId = 0;
57*04fd306cSNickeau
58*04fd306cSNickeau        /**
59*04fd306cSNickeau         * Header Metadata
60*04fd306cSNickeau         *
61*04fd306cSNickeau         * On template that have an header, the h1 and the featured image are
62*04fd306cSNickeau         * captured and deleted by default to allow complex header layout
63*04fd306cSNickeau         *
64*04fd306cSNickeau         * Delete the parsed value (runtime works only on rendering)
65*04fd306cSNickeau         * TODO: move that to the metadata rendering by adding attributes
66*04fd306cSNickeau         *   because if the user changes the template, the parsing will not work
67*04fd306cSNickeau         *   it would need to parse the document again
68*04fd306cSNickeau         */
69*04fd306cSNickeau        $markupPath = $this->outline->getMarkupPath();
70*04fd306cSNickeau        if ($markupPath !== null) {
71*04fd306cSNickeau            FeaturedRasterImage::createFromResourcePage($markupPath)->setParsedValue();
72*04fd306cSNickeau            FeaturedSvgImage::createFromResourcePage($markupPath)->setParsedValue();
73*04fd306cSNickeau        }
74*04fd306cSNickeau        $captureHeaderMeta = $this->outline->getMetaHeaderCapture();
75*04fd306cSNickeau
76*04fd306cSNickeau
77*04fd306cSNickeau        /**
78*04fd306cSNickeau         * Transform and collect the calls in Instructions calls
79*04fd306cSNickeau         */
80*04fd306cSNickeau        $this->toHtmlSectionOutlineCallsRecurse($this->outline->getRootOutlineSection(), $totalCalls, $sectionSequenceId, $captureHeaderMeta);
81*04fd306cSNickeau
82*04fd306cSNickeau        return array_map(function (Call $element) {
83*04fd306cSNickeau            return $element->getInstructionCall();
84*04fd306cSNickeau        }, $totalCalls);
85*04fd306cSNickeau    }
86*04fd306cSNickeau
87*04fd306cSNickeau    /**
88*04fd306cSNickeau     * The visitor, we don't use a visitor pattern for now
89*04fd306cSNickeau     * @param OutlineSection $outlineSection
90*04fd306cSNickeau     * @param array $totalComboCalls
91*04fd306cSNickeau     * @param int $sectionSequenceId
92*04fd306cSNickeau     * @param bool $captureHeaderMeta
93*04fd306cSNickeau     * @return void
94*04fd306cSNickeau     */
95*04fd306cSNickeau    private function toHtmlSectionOutlineCallsRecurse(OutlineSection $outlineSection, array &$totalComboCalls, int &$sectionSequenceId, bool $captureHeaderMeta): void
96*04fd306cSNickeau    {
97*04fd306cSNickeau
98*04fd306cSNickeau        $totalComboCalls[] = Call::createComboCall(
99*04fd306cSNickeau            SectionTag::TAG,
100*04fd306cSNickeau            DOKU_LEXER_ENTER,
101*04fd306cSNickeau            array(HeadingTag::LEVEL => $outlineSection->getLevel()),
102*04fd306cSNickeau            null,
103*04fd306cSNickeau            null,
104*04fd306cSNickeau            null,
105*04fd306cSNickeau            null,
106*04fd306cSNickeau            \syntax_plugin_combo_xmlblocktag::TAG
107*04fd306cSNickeau        );
108*04fd306cSNickeau
109*04fd306cSNickeau        /**
110*04fd306cSNickeau         * In Ads Content Slot Calculation
111*04fd306cSNickeau         */
112*04fd306cSNickeau        $adCalls = [];
113*04fd306cSNickeau        if($this->inArticleEnabled) {
114*04fd306cSNickeau            $adCall = $this->getAdCall($outlineSection);
115*04fd306cSNickeau            if ($adCall !== null) {
116*04fd306cSNickeau                $adCalls = [$adCall];
117*04fd306cSNickeau            }
118*04fd306cSNickeau        }
119*04fd306cSNickeau
120*04fd306cSNickeau        $contentCalls = $outlineSection->getContentCalls();
121*04fd306cSNickeau        if ($outlineSection->hasChildren()) {
122*04fd306cSNickeau
123*04fd306cSNickeau
124*04fd306cSNickeau            $actualChildren = $outlineSection->getChildren();
125*04fd306cSNickeau
126*04fd306cSNickeau            if ($captureHeaderMeta && $outlineSection->getLevel() === 0) {
127*04fd306cSNickeau                // should be only one 1
128*04fd306cSNickeau                if (count($actualChildren) === 1) {
129*04fd306cSNickeau                    $h1Section = $actualChildren[array_key_first($actualChildren)];
130*04fd306cSNickeau                    if ($h1Section->getLevel() === 1) {
131*04fd306cSNickeau                        $h1ContentCalls = $h1Section->getContentCalls();
132*04fd306cSNickeau                        /**
133*04fd306cSNickeau                         * Capture the image if any
134*04fd306cSNickeau                         */
135*04fd306cSNickeau                        if ($this->markupPath !== null) {
136*04fd306cSNickeau                            foreach ($h1ContentCalls as $h1ContentCall) {
137*04fd306cSNickeau                                $tagName = $h1ContentCall->getTagName();
138*04fd306cSNickeau                                switch ($tagName) {
139*04fd306cSNickeau                                    case "p":
140*04fd306cSNickeau                                        continue 2;
141*04fd306cSNickeau                                    case "media":
142*04fd306cSNickeau                                        $h1ContentCall->addAttribute(Display::DISPLAY, Display::DISPLAY_NONE_VALUE);
143*04fd306cSNickeau                                        try {
144*04fd306cSNickeau                                            $fetcher = MediaMarkup::createFromCallStackArray($h1ContentCall->getAttributes())->getFetcher();
145*04fd306cSNickeau                                            switch (get_class($fetcher)) {
146*04fd306cSNickeau                                                case FetcherRaster::class:
147*04fd306cSNickeau                                                    $path = $fetcher->getSourcePath()->toAbsoluteId();
148*04fd306cSNickeau                                                    FeaturedRasterImage::createFromResourcePage($this->markupPath)->setParsedValue($path);
149*04fd306cSNickeau                                                    break;
150*04fd306cSNickeau                                                case FetcherSvg::class:
151*04fd306cSNickeau                                                    $path = $fetcher->getSourcePath()->toAbsoluteId();
152*04fd306cSNickeau                                                    FeaturedSvgImage::createFromResourcePage($this->markupPath)->setParsedValue($path);
153*04fd306cSNickeau                                                    break;
154*04fd306cSNickeau                                            }
155*04fd306cSNickeau                                        } catch (\Exception $e) {
156*04fd306cSNickeau                                            LogUtility::error("Error while capturing the feature images. Error: " . $e->getMessage(), Outline::CANONICAL, $e);
157*04fd306cSNickeau                                        }
158*04fd306cSNickeau                                        continue 2;
159*04fd306cSNickeau                                    default:
160*04fd306cSNickeau                                        // only the images found just after h1
161*04fd306cSNickeau                                        break;
162*04fd306cSNickeau                                }
163*04fd306cSNickeau                            }
164*04fd306cSNickeau                        }
165*04fd306cSNickeau                        $contentCalls = array_merge($contentCalls, $h1ContentCalls);
166*04fd306cSNickeau                        $actualChildren = $h1Section->getChildren();
167*04fd306cSNickeau                    }
168*04fd306cSNickeau                }
169*04fd306cSNickeau            }
170*04fd306cSNickeau
171*04fd306cSNickeau
172*04fd306cSNickeau            /**
173*04fd306cSNickeau             * If header has content,
174*04fd306cSNickeau             * we add it
175*04fd306cSNickeau             */
176*04fd306cSNickeau            $headerHasContent = !(empty($contentCalls) && empty($outlineSection->getHeadingCalls()) && empty($adCall));
177*04fd306cSNickeau            if ($headerHasContent) {
178*04fd306cSNickeau                $totalComboCalls = array_merge(
179*04fd306cSNickeau                    $totalComboCalls,
180*04fd306cSNickeau                    [$this->getOpenHeaderCall()],
181*04fd306cSNickeau                    $outlineSection->getHeadingCalls(),
182*04fd306cSNickeau                    $contentCalls,
183*04fd306cSNickeau                    $adCalls
184*04fd306cSNickeau                );
185*04fd306cSNickeau                $this->addSectionEditButtonComboFormatIfNeeded($outlineSection, $sectionSequenceId, $totalComboCalls);
186*04fd306cSNickeau                $totalComboCalls[] = $this->getCloseHeaderCall();
187*04fd306cSNickeau            }
188*04fd306cSNickeau
189*04fd306cSNickeau            foreach ($actualChildren as $child) {
190*04fd306cSNickeau                $this->toHtmlSectionOutlineCallsRecurse($child, $totalComboCalls, $sectionSequenceId, $captureHeaderMeta);
191*04fd306cSNickeau            }
192*04fd306cSNickeau
193*04fd306cSNickeau        } else {
194*04fd306cSNickeau
195*04fd306cSNickeau            $totalComboCalls = array_merge(
196*04fd306cSNickeau                $totalComboCalls,
197*04fd306cSNickeau                $adCalls,
198*04fd306cSNickeau                $outlineSection->getHeadingCalls(),
199*04fd306cSNickeau                $contentCalls
200*04fd306cSNickeau            );
201*04fd306cSNickeau
202*04fd306cSNickeau            $this->addSectionEditButtonComboFormatIfNeeded($outlineSection, $sectionSequenceId, $totalComboCalls);
203*04fd306cSNickeau        }
204*04fd306cSNickeau
205*04fd306cSNickeau        $totalComboCalls[] = Call::createComboCall(
206*04fd306cSNickeau            SectionTag::TAG,
207*04fd306cSNickeau            DOKU_LEXER_EXIT,
208*04fd306cSNickeau            [],
209*04fd306cSNickeau            null,
210*04fd306cSNickeau            null,
211*04fd306cSNickeau            null,
212*04fd306cSNickeau            null,
213*04fd306cSNickeau            \syntax_plugin_combo_xmlblocktag::TAG
214*04fd306cSNickeau        );
215*04fd306cSNickeau
216*04fd306cSNickeau
217*04fd306cSNickeau    }
218*04fd306cSNickeau
219*04fd306cSNickeau    /**
220*04fd306cSNickeau     * Add the edit button if needed
221*04fd306cSNickeau     * @param $outlineSection
222*04fd306cSNickeau     * @param $sectionSequenceId
223*04fd306cSNickeau     * @param array $totalInstructionCalls
224*04fd306cSNickeau     */
225*04fd306cSNickeau    private
226*04fd306cSNickeau    function addSectionEditButtonComboFormatIfNeeded(OutlineSection $outlineSection, int $sectionSequenceId, array &$totalInstructionCalls): void
227*04fd306cSNickeau    {
228*04fd306cSNickeau        if (!$outlineSection->hasParent()) {
229*04fd306cSNickeau            // no button for the root (ie the page)
230*04fd306cSNickeau            return;
231*04fd306cSNickeau        }
232*04fd306cSNickeau        if ($this->outline->isSectionEditingEnabled()) {
233*04fd306cSNickeau
234*04fd306cSNickeau            $editButton = EditButton::create("Edit the section `{$outlineSection->getLabel()}`")
235*04fd306cSNickeau                ->setStartPosition($outlineSection->getStartPosition())
236*04fd306cSNickeau                ->setEndPosition($outlineSection->getEndPosition());
237*04fd306cSNickeau            if ($outlineSection->hasHeading()) {
238*04fd306cSNickeau                $editButton->setOutlineHeadingId($outlineSection->getHeadingId());
239*04fd306cSNickeau            }
240*04fd306cSNickeau
241*04fd306cSNickeau            $totalInstructionCalls[] = $editButton
242*04fd306cSNickeau                ->setOutlineSectionId($sectionSequenceId)
243*04fd306cSNickeau                ->toComboCallComboFormat();
244*04fd306cSNickeau
245*04fd306cSNickeau        }
246*04fd306cSNickeau
247*04fd306cSNickeau    }
248*04fd306cSNickeau
249*04fd306cSNickeau
250*04fd306cSNickeau    private function getIsLastSectionToBePrinted(OutlineSection $outlineSection): bool
251*04fd306cSNickeau    {
252*04fd306cSNickeau        return $outlineSection === $this->lastSectionToBePrinted;
253*04fd306cSNickeau    }
254*04fd306cSNickeau
255*04fd306cSNickeau    /**
256*04fd306cSNickeau     * @return OutlineSection - the last section to be printed
257*04fd306cSNickeau     */
258*04fd306cSNickeau    private function getLastSectionToBePrinted(): OutlineSection
259*04fd306cSNickeau    {
260*04fd306cSNickeau        return $this->getLastSectionToBePrintedRecurse($this->outline->getRootOutlineSection());
261*04fd306cSNickeau    }
262*04fd306cSNickeau
263*04fd306cSNickeau    private function getLastSectionToBePrintedRecurse(OutlineSection $section): OutlineSection
264*04fd306cSNickeau    {
265*04fd306cSNickeau        $outlineSections = $section->getChildren();
266*04fd306cSNickeau        $array_key_last = array_key_last($outlineSections);
267*04fd306cSNickeau        if ($array_key_last !== null) {
268*04fd306cSNickeau            $lastSection = $outlineSections[$array_key_last];
269*04fd306cSNickeau            return $this->getLastSectionToBePrintedRecurse($lastSection);
270*04fd306cSNickeau        }
271*04fd306cSNickeau        return $section;
272*04fd306cSNickeau    }
273*04fd306cSNickeau
274*04fd306cSNickeau    /**
275*04fd306cSNickeau     * Section Header Creation
276*04fd306cSNickeau     * If it has children and content, wrap the heading and the content
277*04fd306cSNickeau     * in a header tag
278*04fd306cSNickeau     * The header tag helps also to get the edit button to stay in place
279*04fd306cSNickeau     */
280*04fd306cSNickeau    private function getOpenHeaderCall(): Call
281*04fd306cSNickeau    {
282*04fd306cSNickeau
283*04fd306cSNickeau        return Call::createComboCall(
284*04fd306cSNickeau            \syntax_plugin_combo_header::TAG,
285*04fd306cSNickeau            DOKU_LEXER_ENTER,
286*04fd306cSNickeau            array(
287*04fd306cSNickeau                TagAttributes::CLASS_KEY => StyleAttribute::addComboStrapSuffix("outline-header"),
288*04fd306cSNickeau            ),
289*04fd306cSNickeau            Outline::CONTEXT
290*04fd306cSNickeau        );
291*04fd306cSNickeau    }
292*04fd306cSNickeau
293*04fd306cSNickeau    private function getCloseHeaderCall(): Call
294*04fd306cSNickeau    {
295*04fd306cSNickeau        return Call::createComboCall(
296*04fd306cSNickeau            \syntax_plugin_combo_header::TAG,
297*04fd306cSNickeau            DOKU_LEXER_EXIT,
298*04fd306cSNickeau            [],
299*04fd306cSNickeau            Outline::CONTEXT
300*04fd306cSNickeau        );
301*04fd306cSNickeau    }
302*04fd306cSNickeau
303*04fd306cSNickeau    private function getAdCall(OutlineSection $outlineSection): ?Call
304*04fd306cSNickeau    {
305*04fd306cSNickeau        $sectionLineCount = $outlineSection->getLineCount();
306*04fd306cSNickeau        $this->currentLineCountSinceLastAd = $this->currentLineCountSinceLastAd + $sectionLineCount;
307*04fd306cSNickeau        $this->sectionNumbers += 1;
308*04fd306cSNickeau        $isLastSection = $this->getIsLastSectionToBePrinted($outlineSection);
309*04fd306cSNickeau        if (AdTag::showAds(
310*04fd306cSNickeau            $sectionLineCount,
311*04fd306cSNickeau            $this->currentLineCountSinceLastAd,
312*04fd306cSNickeau            $this->sectionNumbers,
313*04fd306cSNickeau            $this->adsCounter,
314*04fd306cSNickeau            $isLastSection,
315*04fd306cSNickeau            $this->outline->getMarkupPath()
316*04fd306cSNickeau        )) {
317*04fd306cSNickeau
318*04fd306cSNickeau            // Number of ads inserted
319*04fd306cSNickeau            $this->adsCounter += 1;
320*04fd306cSNickeau            // Reset the number of line betwee the last ad
321*04fd306cSNickeau            $this->currentLineCountSinceLastAd = 0;
322*04fd306cSNickeau
323*04fd306cSNickeau            return Call::createComboCall(
324*04fd306cSNickeau                AdTag::MARKUP,
325*04fd306cSNickeau                DOKU_LEXER_SPECIAL,
326*04fd306cSNickeau                array(AdTag::NAME_ATTRIBUTE => AdTag::PREFIX_IN_ARTICLE_ADS . $this->adsCounter),
327*04fd306cSNickeau                Outline::CONTEXT,
328*04fd306cSNickeau                null,
329*04fd306cSNickeau                null,
330*04fd306cSNickeau                null,
331*04fd306cSNickeau                \syntax_plugin_combo_xmlblockemptytag::TAG
332*04fd306cSNickeau            );
333*04fd306cSNickeau        }
334*04fd306cSNickeau        return null;
335*04fd306cSNickeau    }
336*04fd306cSNickeau}
337