1<?php 2 3 4namespace ComboStrap; 5 6 7use syntax_plugin_combo_frontmatter; 8 9class MetadataFrontmatterStore extends MetadataSingleArrayStore 10{ 11 12 const NAME = "frontmatter"; 13 const CANONICAL = self::NAME; 14 15 /** 16 * @var bool Do we have a frontmatter on the page 17 */ 18 private $isPresent = false; 19 /** 20 * @var string 21 */ 22 private $contentWithoutFrontMatter; 23 24 /** 25 * @throws ExceptionCombo 26 */ 27 private function syncData() 28 { 29 30 /** 31 * @var Page $resourceCombo 32 */ 33 $resourceCombo = $this->getResource(); 34 35 /** 36 * Resource Id special 37 */ 38 $guidObject = $resourceCombo->getUidObject(); 39 if ( 40 !$this->hasProperty($guidObject::getPersistentName()) 41 && 42 $guidObject->getValue() !== null 43 ) { 44 $this->setFromPersistentName($guidObject::getPersistentName(), $guidObject->getValue()); 45 } 46 47 /** 48 * Read store 49 */ 50 $dokuwikiStore = MetadataDokuWikiStore::getOrCreateFromResource($resourceCombo); 51 $metaFilePath = $dokuwikiStore->getMetaFilePath(); 52 if ($metaFilePath !== null) { 53 $metaModifiedTime = FileSystems::getModifiedTime($metaFilePath); 54 $pageModifiedTime = FileSystems::getModifiedTime($resourceCombo->getPath()); 55 $diff = $pageModifiedTime->diff($metaModifiedTime); 56 if ($diff === false) { 57 throw new ExceptionCombo("Unable to calculate the diff between the page and metadata file"); 58 } 59 $secondDiff = intval($diff->format('%s')); 60 if ($secondDiff > 0) { 61 $resourceCombo->renderMetadataAndFlush(); 62 } 63 } 64 /** 65 * Update the mutable data 66 * (ie delete insert) 67 */ 68 foreach (Metadata::MUTABLE_METADATA as $metaKey) { 69 $metadata = Metadata::getForName($metaKey); 70 if ($metadata === null) { 71 $msg = "The metadata $metaKey should be defined"; 72 if (PluginUtility::isDevOrTest()) { 73 throw new ExceptionCombo($msg); 74 } else { 75 LogUtility::msg($msg); 76 } 77 } 78 $metadata 79 ->setResource($resourceCombo) 80 ->setReadStore($dokuwikiStore) 81 ->setWriteStore($this); 82 83 $sourceValue = $this->get($metadata); 84 $targetValue = $metadata->getValue(); 85 $defaultValue = $metadata->getDefaultValue(); 86 /** 87 * Strict because otherwise the comparison `false = null` is true 88 */ 89 $targetValueShouldBeStore = !in_array($targetValue, [$defaultValue, null], true); 90 if ($targetValueShouldBeStore) { 91 if ($sourceValue !== $targetValue) { 92 $this->set($metadata); 93 } 94 } else { 95 if ($sourceValue !== null) { 96 $this->remove($metadata); 97 } 98 } 99 } 100 } 101 102 /** 103 * Update the frontmatter with the managed metadata 104 * Used after a submit from the form 105 * @return Message 106 */ 107 public function sync(): Message 108 { 109 110 /** 111 * Default update value for the frontmatter 112 */ 113 $updateFrontMatter = PluginUtility::getConfValue(syntax_plugin_combo_frontmatter::CONF_ENABLE_FRONT_MATTER_ON_SUBMIT, syntax_plugin_combo_frontmatter::CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT); 114 115 116 if ($this->isPresent()) { 117 $updateFrontMatter = 1; 118 } 119 120 121 if ($updateFrontMatter === 0) { 122 return Message::createInfoMessage("The frontmatter is not enabled") 123 ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_NOT_ENABLED); 124 } 125 126 try { 127 $this->syncData(); 128 } catch (ExceptionCombo $e) { 129 return Message::createInfoMessage($e->getMessage()) 130 ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_ERROR); 131 } 132 133 134 /** 135 * Same ? 136 */ 137 if (!$this->hasStateChanged()) { 138 return Message::createInfoMessage("The frontmatter are the same (no update)") 139 ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_NOT_CHANGED); 140 } 141 142 $this->persist(); 143 144 return Message::createInfoMessage("The frontmatter was changed") 145 ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_DONE); 146 147 } 148 149 150 public function isPresent(): bool 151 { 152 return $this->isPresent; 153 } 154 155 /** 156 * MetadataFrontmatterStore constructor. 157 * @param ResourceCombo $page 158 * @param array|null $data 159 */ 160 public function __construct(ResourceCombo $page, array $data = null) 161 { 162 parent::__construct($page, $data); 163 } 164 165 /** 166 * @param $match 167 * @return array|null - null if decodage problem, empty array if no json or an associative array 168 * @deprecated used {@link MetadataFrontmatterStore::loadAsString()} instead 169 */ 170 public static function frontMatterMatchToAssociativeArray($match): ?array 171 { 172 $jsonString = self::stripFrontmatterTag($match); 173 174 // Empty front matter 175 if (trim($jsonString) == "") { 176 return []; 177 } 178 179 // Otherwise you get an object ie $arrayFormat-> syntax 180 $arrayFormat = true; 181 return json_decode($jsonString, $arrayFormat); 182 } 183 184 public static function stripFrontmatterTag($match) 185 { 186 // strip 187 // from start `---json` + eol = 8 188 // from end `---` + eol = 4 189 return substr($match, 7, -3); 190 } 191 192 193 public static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore 194 { 195 return new MetadataFrontmatterStore($resourceCombo, null); 196 } 197 198 public static function createFromArray(ResourceCombo $page, array $jsonArray): MetadataFrontmatterStore 199 { 200 return new MetadataFrontmatterStore($page, $jsonArray); 201 } 202 203 /** 204 * @throws ExceptionCombo 205 */ 206 public static function createFromFrontmatterString($page, $frontmatter = null): MetadataFrontmatterStore 207 { 208 if ($frontmatter === null) { 209 return new MetadataFrontmatterStore($page, []); 210 } 211 $jsonArray = self::frontMatterMatchToAssociativeArray($frontmatter); 212 if ($jsonArray === null) { 213 throw new ExceptionCombo("The frontmatter is not valid"); 214 } 215 return new MetadataFrontmatterStore($page, $jsonArray); 216 } 217 218 /** 219 * @throws ExceptionCombo 220 */ 221 public static function createFromPage(Page $page): MetadataFrontmatterStore 222 { 223 $content = FileSystems::getContent($page->getPath()); 224 $frontMatterStartTag = syntax_plugin_combo_frontmatter::START_TAG; 225 if (strpos($content, $frontMatterStartTag) === 0) { 226 227 /** 228 * Extract the actual values 229 */ 230 $pattern = syntax_plugin_combo_frontmatter::PATTERN; 231 $split = preg_split("/($pattern)/ms", $content, 2, PREG_SPLIT_DELIM_CAPTURE); 232 233 /** 234 * The split normally returns an array 235 * where the first element is empty followed by the frontmatter 236 */ 237 $emptyString = array_shift($split); 238 if (!empty($emptyString)) { 239 throw new ExceptionCombo("The frontmatter is not the first element"); 240 } 241 242 $frontMatterMatch = array_shift($split); 243 /** 244 * Building the document again 245 */ 246 $contentWithoutFrontMatter = ""; 247 while (($element = array_shift($split)) != null) { 248 $contentWithoutFrontMatter .= $element; 249 } 250 251 return MetadataFrontmatterStore::createFromFrontmatterString($page, $frontMatterMatch) 252 ->setIsPresent(true) 253 ->setContentWithoutFrontMatter($contentWithoutFrontMatter); 254 255 } 256 return (new MetadataFrontmatterStore($page)) 257 ->setIsPresent(false) 258 ->setContentWithoutFrontMatter($content); 259 260 } 261 262 263 public function __toString() 264 { 265 return self::NAME; 266 } 267 268 269 public function getJsonString(): string 270 { 271 272 $jsonArray = $this->getData(); 273 ksort($jsonArray); 274 return self::toFrontmatterJsonString($jsonArray); 275 276 } 277 278 /** 279 * This formatting make the object on one line for a list of object 280 * making the frontmatter compacter (one line, one meta) 281 * @param $jsonArray 282 * @return string 283 */ 284 public static function toFrontmatterJsonString($jsonArray): string 285 { 286 287 if (sizeof($jsonArray) === 0) { 288 return "{}"; 289 } 290 $jsonString = ""; 291 self::jsonFlatRecursiveEncoding($jsonArray, $jsonString); 292 293 /** 294 * Double Guard (frontmatter should be quick enough) 295 * to support this overhead 296 */ 297 $decoding = json_decode($jsonString); 298 if ($decoding === null) { 299 throw new ExceptionComboRuntime("The generated frontmatter json is no a valid json"); 300 } 301 return $jsonString; 302 } 303 304 private static function jsonFlatRecursiveEncoding(array $jsonProperty, &$jsonString, $level = 0, $endOfFieldCharacter = DOKU_LF, $type = Json::TYPE_OBJECT, $parentType = Json::TYPE_OBJECT) 305 { 306 /** 307 * Open the root object 308 */ 309 if ($type === Json::TYPE_OBJECT) { 310 $jsonString .= "{"; 311 } else { 312 $jsonString .= "["; 313 } 314 315 /** 316 * Level indentation 317 */ 318 $levelSpaceIndentation = str_repeat(" ", ($level + 1) * Json::TAB_SPACES_COUNTER); 319 320 /** 321 * Loop 322 */ 323 $elementCounter = 0; 324 foreach ($jsonProperty as $key => $value) { 325 326 $elementCounter++; 327 328 /** 329 * Close the previous property 330 */ 331 $isFirstProperty = $elementCounter === 1; 332 if ($isFirstProperty && $parentType !== Json::PARENT_TYPE_ARRAY) { 333 // go the line if this is not a list of object 334 $jsonString .= DOKU_LF; 335 } 336 if (!$isFirstProperty) { 337 $jsonString .= ",$endOfFieldCharacter"; 338 } 339 if ($endOfFieldCharacter === DOKU_LF) { 340 $tab = $levelSpaceIndentation; 341 } else { 342 $tab = " "; 343 } 344 $jsonString .= $tab; 345 346 /** 347 * Recurse 348 */ 349 $jsonEncodedKey = json_encode($key); 350 if (is_array($value)) { 351 $childLevel = $level + 1; 352 if (is_numeric($key)) { 353 /** 354 * List of object 355 */ 356 $childType = Json::TYPE_OBJECT; 357 $childEndOField = ""; 358 } else { 359 /** 360 * Array 361 */ 362 $jsonString .= "$jsonEncodedKey: "; 363 $childType = Json::TYPE_OBJECT; 364 if ($value[0] !== null) { 365 $childType = Json::PARENT_TYPE_ARRAY; 366 } 367 $childEndOField = $endOfFieldCharacter; 368 } 369 self::jsonFlatRecursiveEncoding($value, $jsonString, $childLevel, $childEndOField, $childType, $type); 370 371 } else { 372 /** 373 * Single property 374 */ 375 $jsonEncodedValue = json_encode($value); 376 $jsonString .= "$jsonEncodedKey: $jsonEncodedValue"; 377 378 } 379 380 } 381 382 /** 383 * Close the object or array 384 */ 385 $closingLevelSpaceIndentation = str_repeat(" ", $level * Json::TAB_SPACES_COUNTER); 386 if ($type === Json::TYPE_OBJECT) { 387 if ($parentType !== Json::PARENT_TYPE_ARRAY) { 388 $jsonString .= DOKU_LF . $closingLevelSpaceIndentation; 389 } else { 390 $jsonString .= " "; 391 } 392 $jsonString .= "}"; 393 } else { 394 /** 395 * The array is not going one level back 396 */ 397 $jsonString .= DOKU_LF . $closingLevelSpaceIndentation . "]"; 398 } 399 } 400 401 402 public function toFrontmatterString(): string 403 { 404 $frontmatterStartTag = syntax_plugin_combo_frontmatter::START_TAG; 405 $frontmatterEndTag = syntax_plugin_combo_frontmatter::END_TAG; 406 $jsonArray = $this->getData(); 407 ksort($jsonArray); 408 $jsonEncode = self::toFrontmatterJsonString($jsonArray); 409 410 return <<<EOF 411$frontmatterStartTag 412$jsonEncode 413$frontmatterEndTag 414EOF; 415 416 417 } 418 419 private function setIsPresent(bool $bool): MetadataFrontmatterStore 420 { 421 $this->isPresent = $bool; 422 return $this; 423 } 424 425 public function persist() 426 { 427 if ($this->contentWithoutFrontMatter === null) { 428 LogUtility::msg("The content without frontmatter should have been set. Did you you use the createFromPage constructor"); 429 return $this; 430 } 431 $targetFrontMatterJsonString = $this->toFrontmatterString(); 432 433 /** 434 * EOL for the first frontmatter 435 */ 436 $sep = ""; 437 if (strlen($this->contentWithoutFrontMatter) > 0) { 438 $firstChar = $this->contentWithoutFrontMatter[0]; 439 if (!in_array($firstChar, ["\n", "\r"])) { 440 $sep = "\n"; 441 } 442 } 443 444 /** 445 * Build the new document 446 */ 447 $newPageContent = <<<EOF 448$targetFrontMatterJsonString$sep$this->contentWithoutFrontMatter 449EOF; 450 $resourceCombo = $this->getResource(); 451 if ($resourceCombo instanceof Page) { 452 $resourceCombo->upsertContent($newPageContent, "Metadata frontmatter store upsert"); 453 } 454 return $this; 455 } 456 457 private function setContentWithoutFrontMatter(string $contentWithoutFrontMatter): MetadataFrontmatterStore 458 { 459 $this->contentWithoutFrontMatter = $contentWithoutFrontMatter; 460 return $this; 461 } 462 463 464} 465