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