1<?php 2 3namespace ComboStrap; 4 5use dokuwiki\Cache\CacheInstructions; 6use dokuwiki\Cache\CacheParser; 7use dokuwiki\Cache\CacheRenderer; 8 9/** 10 * Builder class for {@link FetcherMarkup} 11 * Php does not allow for nested class 12 * We therefore need to get the builder class out. 13 * 14 * We extends just to get access to protected members class 15 * and to mimic a builder pattern 16 * 17 * @internal 18 */ 19class FetcherMarkupBuilder 20{ 21 22 /** 23 * Private are they may be null 24 */ 25 private ?string $builderMarkupString = null; 26 private ?Path $builderMarkupSourcePath = null; 27 private ?array $builderRequestedInstructions = null; 28 29 protected WikiPath $requestedContextPath; 30 protected Mime $mime; 31 protected bool $deleteRootBlockElement = false; 32 protected string $rendererName; 33 34 35 protected bool $isDoc; 36 protected array $builderContextData; 37 private bool $isCodeStandAloneExecution = false; 38 /** 39 * @var FetcherMarkup - a parent if any 40 */ 41 private FetcherMarkup $parentMarkupHandler; 42 43 44 public function __construct() 45 { 46 } 47 48 /** 49 * The local path is part of the key cache and should be the same 50 * than dokuwiki 51 * 52 * For whatever reason, Dokuwiki uses: 53 * * `/` as separator on Windows 54 * * and Windows short path `GERARD~1` not gerardnico 55 * See {@link wikiFN()} 56 * There is also a cache in the function 57 * 58 * We can't use our {@link Path} class to be compatible because the 59 * path is on windows format without the short path format 60 */ 61 public static function getWikiIdAndLocalFileDokuwikiCompliant(Path $sourcePath): array 62 { 63 64 try { 65 $markuSourceWikiPath = $sourcePath->toWikiPath(); 66 67 if ($markuSourceWikiPath->getDrive() === WikiPath::MARKUP_DRIVE) { 68 /** 69 * Dokuwiki special function 70 * that should be the same to conform to the cache key 71 */ 72 $wikiId = $markuSourceWikiPath->getWikiId(); 73 $localFile = wikiFN($wikiId); 74 } else { 75 $localFile = $markuSourceWikiPath->toLocalPath(); 76 $wikiId = $markuSourceWikiPath->toUriString(); 77 } 78 } catch (ExceptionCast $e) { 79 $wikiId = $sourcePath->toAbsoluteId(); 80 try { 81 $localFile = $sourcePath->toLocalPath(); 82 } catch (ExceptionCast $e) { 83 throw new ExceptionRuntimeInternal("The source path ({$sourcePath}) is not supported as markup source path.", $e); 84 } 85 } 86 return [$wikiId, $localFile]; 87 } 88 89 /** 90 * @param string $markupString - the markup is a string format 91 * @return FetcherMarkupBuilder 92 */ 93 public function setRequestedMarkupString(string $markupString): FetcherMarkupBuilder 94 { 95 $this->builderMarkupString = $markupString; 96 return $this; 97 } 98 99 /** 100 * Delete the first P instructions 101 * (The parser will add a p block element) 102 * @param bool $b 103 * @return $this 104 */ 105 public function setDeleteRootBlockElement(bool $b): FetcherMarkupBuilder 106 { 107 $this->deleteRootBlockElement = $b; 108 return $this; 109 } 110 111 /** 112 * The source where the markup is stored (null if dynamic) 113 * It's a duplicate of {@link FetcherMarkup::setSourcePath()} 114 * @param ?Path $executingPath 115 * @return $this 116 */ 117 public function setRequestedExecutingPath(?Path $executingPath): FetcherMarkupBuilder 118 { 119 120 if ($executingPath == null) { 121 return $this; 122 } 123 124 try { 125 /** 126 * Normalize to wiki path if possible 127 * Why ? 128 * Because the parent path may be used a {@link MarkupCacheDependencies::getValueForKey() cache key} 129 * and they will have different value if the path type is different 130 * * With {@link LocalPath Local Path}: `C:\Users\gerardnico\AppData\Local\Temp\dwtests-1676386702.9751\data\pages\ns_without_scope` 131 * * With {@link WikiPath Wiki Path}: `ns_without_scope` 132 * It will then make the cache file path different (ie the md5 output key is the file name) 133 */ 134 $this->builderMarkupSourcePath = $executingPath->toWikiPath(); 135 } catch (ExceptionCast $e) { 136 $this->builderMarkupSourcePath = $executingPath; 137 } 138 return $this; 139 140 } 141 142 /** 143 * The page context in which this fragment was requested 144 * 145 * Note that it may or may be not the main requested markup page. 146 * You can have a markup rendering inside another markup rendering. 147 * 148 * @param WikiPath $contextPath 149 * @return $this 150 */ 151 public function setRequestedContextPath(WikiPath $contextPath): FetcherMarkupBuilder 152 { 153 $this->requestedContextPath = $contextPath; 154 return $this; 155 } 156 157 /** 158 */ 159 public function setRequestedMime(Mime $mime): FetcherMarkupBuilder 160 { 161 $this->mime = $mime; 162 return $this; 163 } 164 165 public function setRequestedMimeToXhtml(): FetcherMarkupBuilder 166 { 167 try { 168 return $this->setRequestedMime(Mime::createFromExtension("xhtml")); 169 } catch (ExceptionNotFound $e) { 170 throw new ExceptionRuntime("Internal error", 0, $e); 171 } 172 173 } 174 175 176 /** 177 * Technically, you could set the mime to whatever you want 178 * and still get the instructions via {@link FetcherMarkup::getInstructions()} 179 * Setting the mime to instructions will just not do any render processing. 180 * @return $this 181 */ 182 public function setRequestedMimeToInstructions(): FetcherMarkupBuilder 183 { 184 try { 185 $this->setRequestedMime(Mime::createFromExtension(MarkupRenderer::INSTRUCTION_EXTENSION)); 186 } catch (ExceptionNotFound $e) { 187 throw new ExceptionRuntime("Internal error: the mime is internal and should be good"); 188 } 189 return $this; 190 191 } 192 193 194 /** 195 * @throws ExceptionNotExists 196 */ 197 public function build(): FetcherMarkup 198 { 199 200 /** 201 * One input should be given 202 */ 203 if ($this->builderMarkupSourcePath === null && $this->builderMarkupString === null && $this->builderRequestedInstructions === null) { 204 throw new ExceptionRuntimeInternal("A markup source path, a markup string or instructions should be given"); 205 } 206 /** 207 * Only one input should be given 208 */ 209 $foundInput = ""; 210 if ($this->builderMarkupSourcePath !== null) { 211 $foundInput = "markup path"; 212 } 213 if ($this->builderMarkupString !== null) { 214 if (!empty($foundInput)) { 215 throw new ExceptionRuntimeInternal("Only one input should be given, we have found 2 inputs ($foundInput and markup string)"); 216 } 217 $foundInput = "markup string"; 218 219 } 220 if ($this->builderRequestedInstructions !== null) { 221 if (!empty($foundInput)) { 222 throw new ExceptionRuntimeInternal("Only one input should be given, we have found 2 inputs ($foundInput and instructions)"); 223 } 224 } 225 226 /** 227 * Other Mandatory 228 */ 229 if (!isset($this->mime)) { 230 throw new ExceptionRuntimeInternal("A mime is mandatory"); 231 } 232 if (!isset($this->requestedContextPath)) { 233 throw new ExceptionRuntimeInternal("A context path is mandatory"); 234 } 235 236 /** 237 * The object type is mandatory 238 */ 239 if (!isset($this->rendererName)) { 240 switch ($this->mime->toString()) { 241 case Mime::XHTML: 242 /** 243 * ie last name of {@link \Doku_Renderer_xhtml} 244 */ 245 $rendererName = MarkupRenderer::XHTML_RENDERER; 246 break; 247 case Mime::META: 248 /** 249 * ie last name of {@link \Doku_Renderer_metadata} 250 */ 251 $rendererName = "metadata"; 252 break; 253 case Mime::INSTRUCTIONS: 254 /** 255 * Does not exist yet bu that the future 256 */ 257 $rendererName = FetcherMarkupInstructions::NAME; 258 break; 259 default: 260 throw new ExceptionRuntimeInternal("A renderer name (ie builder name/output object type) is mandatory"); 261 } 262 } else { 263 $rendererName = $this->rendererName; 264 } 265 266 /** 267 * Building 268 */ 269 $newFetcherMarkup = new FetcherMarkup(); 270 $newFetcherMarkup->builderName = $rendererName; 271 272 $newFetcherMarkup->requestedContextPath = $this->requestedContextPath; 273 if ($this->builderMarkupString !== null) { 274 $newFetcherMarkup->markupString = $this->builderMarkupString; 275 } 276 if ($this->builderMarkupSourcePath !== null) { 277 $newFetcherMarkup->markupSourcePath = $this->builderMarkupSourcePath; 278 if (!FileSystems::exists($this->builderMarkupSourcePath)) { 279 /** 280 * Too much edge case for now with dokuwiki 281 * The {@link \Doku_Renderer_metadata} for instance throws an error if the file does not exist 282 * ... etc .... 283 */ 284 throw new ExceptionNotExists("The executing source file ({$this->builderMarkupSourcePath}) does not exist"); 285 } 286 } 287 if ($this->builderRequestedInstructions !== null) { 288 $newFetcherMarkup->requestedInstructions = $this->builderRequestedInstructions; 289 } 290 $newFetcherMarkup->mime = $this->mime; 291 $newFetcherMarkup->deleteRootBlockElement = $this->deleteRootBlockElement; 292 293 294 $newFetcherMarkup->isDoc = $this->getIsDocumentExecution(); 295 if (isset($this->builderContextData)) { 296 $newFetcherMarkup->contextData = $this->builderContextData; 297 } 298 299 if (isset($this->parentMarkupHandler)) { 300 $newFetcherMarkup->parentMarkupHandler = $this->parentMarkupHandler; 301 } 302 $newFetcherMarkup->isNonPathStandaloneExecution = $this->isCodeStandAloneExecution; 303 304 305 /** 306 * We build the cache dependencies even if there is no source markup path (therefore no cache store) 307 * (Why ? for test purpose, where we want to check if the dependencies was applied) 308 * !!! Attention, the build of the dependencies should happen after that the markup source path is set !!! 309 */ 310 $newFetcherMarkup->outputCacheDependencies = MarkupCacheDependencies::create($newFetcherMarkup); 311 312 /** 313 * The cache object depends on the running request 314 * We build it then just 315 * 316 * A request is also send by dokuwiki to check the cache validity 317 * 318 */ 319 if ($this->builderMarkupSourcePath !== null) { 320 321 322 list($wikiId, $localFile) = self::getWikiIdAndLocalFileDokuwikiCompliant($this->builderMarkupSourcePath); 323 324 /** 325 * Instructions cache 326 */ 327 $newFetcherMarkup->instructionsCache = new CacheInstructions($wikiId, $localFile); 328 329 /** 330 * Content cache 331 */ 332 $extension = $this->mime->getExtension(); 333 $newFetcherMarkup->contentCache = new CacheRenderer($wikiId, $localFile, $extension); 334 $newFetcherMarkup->outputCacheDependencies->rerouteCacheDestination($newFetcherMarkup->contentCache); 335 336 /** 337 * Snippet Cache 338 * Snippet.json is data dependent 339 * 340 * For instance, the carrousel may add glide or grid as snippet. It depends on the the number of backlinks. 341 * 342 * Therefore the output should be unique by rendered slot 343 * Therefore we reroute (recalculate the cache key to the same than the html file) 344 */ 345 $newFetcherMarkup->snippetCache = new CacheParser($wikiId, $localFile, "snippet.json"); 346 $newFetcherMarkup->outputCacheDependencies->rerouteCacheDestination($newFetcherMarkup->snippetCache); 347 348 349 /** 350 * Runtime Meta cache 351 * (Technically, it's derived from the instructions) 352 */ 353 $newFetcherMarkup->metaPath = LocalPath::createFromPathString(metaFN($wikiId, '.meta')); 354 $newFetcherMarkup->metaCache = new CacheRenderer($wikiId, $localFile, 'metadata'); 355 356 } 357 358 return $newFetcherMarkup; 359 360 } 361 362 public function setRequestedMimeToMetadata(): FetcherMarkupBuilder 363 { 364 try { 365 return $this->setRequestedMime(Mime::createFromExtension(MarkupRenderer::METADATA_EXTENSION)); 366 } catch (ExceptionNotFound $e) { 367 throw new ExceptionRuntime("Internal error", 0, $e); 368 } 369 } 370 371 public function setRequestedRenderer(string $rendererName): FetcherMarkupBuilder 372 { 373 $this->rendererName = $rendererName; 374 return $this; 375 } 376 377 public function setRequestedContextPathWithDefault(): FetcherMarkupBuilder 378 { 379 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 380 try { 381 // do we have an executing handler 382 $this->requestedContextPath = $executionContext 383 ->getExecutingMarkupHandler() 384 ->getRequestedExecutingPath() 385 ->toWikiPath(); 386 } catch (ExceptionCast|ExceptionNotFound $e) { 387 $this->requestedContextPath = $executionContext->getConfig()->getDefaultContextPath(); 388 } 389 return $this; 390 } 391 392 /** 393 * @param bool $isDoc - if the markup is a document or a fragment 394 * @return $this 395 * If the markup is a document, an outline is added, a toc is calculated. 396 * 397 * The default is execution parameters dependent if not set 398 * and is calculated at {@link FetcherMarkupBuilder::getIsDocumentExecution()} 399 */ 400 public function setIsDocument(bool $isDoc): FetcherMarkupBuilder 401 { 402 $this->isDoc = $isDoc; 403 return $this; 404 } 405 406 /** 407 * @param array $instructions 408 * @return FetcherMarkupBuilder 409 */ 410 public function setRequestedInstructions(array $instructions): FetcherMarkupBuilder 411 { 412 $this->builderRequestedInstructions = $instructions; 413 return $this; 414 } 415 416 /** 417 * @param array|null $contextData 418 * @return $this 419 */ 420 public function setContextData(?array $contextData): FetcherMarkupBuilder 421 { 422 if ($contextData == null) { 423 return $this; 424 } 425 $this->builderContextData = $contextData; 426 return $this; 427 } 428 429 /** 430 * @param bool $isStandAlone 431 * @return $this 432 * 433 * when a execution is not a {@link FetcherMarkup::isPathExecution()}, the snippet will not be stored automatically. 434 * To avoid this problem, a warning is send if the calling code does not set explicitly that this is specifically a 435 * standalone execution 436 */ 437 public function setIsStandAloneCodeExecution(bool $isStandAlone): FetcherMarkupBuilder 438 { 439 $this->isCodeStandAloneExecution = $isStandAlone; 440 return $this; 441 } 442 443 /** 444 * Determins if the run is a fragment or document execution 445 * 446 * Note: in dokuwiki term, a {@link ExecutionContext::PREVIEW_ACTION} 447 * preview action is a fragment 448 * 449 * @return bool true if this is a document execution 450 */ 451 private function getIsDocumentExecution(): bool 452 { 453 454 if (isset($this->isDoc)) { 455 return $this->isDoc; 456 } 457 458 /** 459 * By default, a string is not a whole doc 460 * (in test, this is almost always the case) 461 */ 462 if ($this->builderMarkupString !== null) { 463 return false; 464 } 465 466 /** 467 * By default, a instructions array is not a whole doc 468 * (in test and rendering, this is almost always the case) 469 */ 470 if ($this->builderRequestedInstructions !== null) { 471 return false; 472 } 473 474 try { 475 476 /** 477 * What fucked up is fucked up 478 * Fragment such as sidebar may run in their own context (ie when editing for instance) 479 * but are not a document 480 */ 481 $executingWikiPath = $this->builderMarkupSourcePath->toWikiPath(); 482 $isFragmentMarkup = MarkupPath::createPageFromPathObject($executingWikiPath)->isSlot(); 483 if ($isFragmentMarkup) { 484 return false; 485 } 486 487 /** 488 * If the context and executing path are: 489 * * the same, this is a document run 490 * * not the same, this is a fragment run 491 */ 492 if ($this->requestedContextPath->toUriString() !== $executingWikiPath->toUriString()) { 493 return false; 494 } 495 496 } catch (ExceptionCast $e) { 497 // no executing path, not a wiki path 498 } 499 500 return true; 501 502 } 503 504 public function setParentMarkupHandler(FetcherMarkup $parentMarkupHandler): FetcherMarkupBuilder 505 { 506 $this->parentMarkupHandler = $parentMarkupHandler; 507 return $this; 508 } 509 510 511} 512