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