1<?php 2/** 3 * Front Matter implementation to add metadata 4 * 5 * 6 * that enhance the metadata dokuwiki system 7 * https://www.dokuwiki.org/metadata 8 * that use the Dublin Core Standard 9 * http://dublincore.org/ 10 * by adding the front matter markup specification 11 * https://gerardnico.com/markup/front-matter 12 * 13 * Inspiration 14 * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php 15 * https://www.dokuwiki.org/plugin:semantic 16 * 17 * See also structured plugin 18 * https://www.dokuwiki.org/plugin:data 19 * https://www.dokuwiki.org/plugin:struct 20 * 21 */ 22 23use ComboStrap\ExceptionBadArgument; 24use ComboStrap\ExceptionBadSyntax; 25use ComboStrap\ExceptionCompile; 26use ComboStrap\ExceptionNotFound; 27use ComboStrap\ExceptionRuntime; 28use ComboStrap\ExecutionContext; 29use ComboStrap\LogUtility; 30use ComboStrap\MarkupPath; 31use ComboStrap\MarkupRef; 32use ComboStrap\MediaMarkup; 33use ComboStrap\Meta\Api\Metadata; 34use ComboStrap\Meta\Api\MetadataSystem; 35use ComboStrap\Meta\Store\MetadataDokuWikiStore; 36use ComboStrap\MetadataFrontmatterStore; 37use ComboStrap\MetadataStoreTransfer; 38use ComboStrap\PageDescription; 39use ComboStrap\Meta\Field\PageImagePath; 40use ComboStrap\Meta\Field\PageImages; 41use ComboStrap\PluginUtility; 42 43require_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 44 45 46/** 47 * All DokuWiki plugins to extend the parser/rendering mechanism 48 * need to inherit from this class 49 * 50 * For a list of meta, see also https://ghost.org/docs/publishing/#api-data 51 */ 52class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin 53{ 54 55 const PARSING_STATE_ERROR = 1; 56 const PARSING_STATE_SUCCESSFUL = 0; 57 58 const CANONICAL = "frontmatter"; 59 const TAG = "frontmatter"; 60 const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT = 0; 61 62 /** 63 * Used in the move plugin 64 * !!! The two last word of the plugin class !!! 65 */ 66 const COMPONENT = 'combo_' . self::CANONICAL; 67 const START_TAG = '---json'; 68 const END_TAG = '---'; 69 const METADATA_IMAGE_CANONICAL = "metadata:image"; 70 const PATTERN = self::START_TAG . '.*?' . self::END_TAG; 71 72 /** 73 * The update status for the update of the frontmatter 74 */ 75 const UPDATE_EXIT_CODE_DONE = 000; 76 const UPDATE_EXIT_CODE_NOT_ENABLED = 100; 77 const UPDATE_EXIT_CODE_NOT_CHANGED = 200; 78 const UPDATE_EXIT_CODE_ERROR = 500; 79 80 81 /** 82 * Syntax Type. 83 * 84 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 85 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 86 * 87 * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 88 * 89 * baseonly - run only in the base 90 */ 91 function getType(): string 92 { 93 return 'baseonly'; 94 } 95 96 public function getPType(): string 97 { 98 /** 99 * This element create a section 100 * element that is a div 101 * that should not be in paragraph 102 * 103 * We make it a block 104 */ 105 return "block"; 106 } 107 108 109 /** 110 * @see Doku_Parser_Mode::getSort() 111 * Higher number than the teaser-columns 112 * because the mode with the lowest sort number will win out 113 */ 114 function getSort() 115 { 116 return 99; 117 } 118 119 /** 120 * Create a pattern that will called this plugin 121 * 122 * @param string $mode 123 * @see Doku_Parser_Mode::connectTo() 124 */ 125 function connectTo($mode) 126 { 127 if ($mode == "base") { 128 // only from the top 129 $this->Lexer->addSpecialPattern(self::PATTERN, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 130 } 131 } 132 133 /** 134 * 135 * The handle function goal is to parse the matched syntax through the pattern function 136 * and to return the result for use in the renderer 137 * This result is always cached until the page is modified. 138 * @param string $match 139 * @param int $state 140 * @param int $pos 141 * @param Doku_Handler $handler 142 * @return array 143 * @see DokuWiki_Syntax_Plugin::handle() 144 * 145 */ 146 function handle($match, $state, $pos, Doku_Handler $handler): array 147 { 148 149 $result = []; 150 151 try { 152 $wikiPath = ExecutionContext::getActualOrCreateFromEnv()->getExecutingWikiPath(); 153 $parsedPage = MarkupPath::createPageFromPathObject($wikiPath); 154 } catch (ExceptionCompile $e) { 155 LogUtility::error("The global ID is unknown, we couldn't get the requested page", self::CANONICAL); 156 return []; 157 } 158 try { 159 160 $frontMatterStore = MetadataFrontmatterStore::createFromFrontmatterString($parsedPage, $match); 161 $result[PluginUtility::EXIT_CODE] = self::PARSING_STATE_SUCCESSFUL; 162 } catch (ExceptionCompile $e) { 163 // Decode problem 164 $result[PluginUtility::EXIT_CODE] = self::PARSING_STATE_ERROR; 165 $result[PluginUtility::EXIT_MESSAGE] = $match; 166 return $result; 167 } 168 169 170 $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($parsedPage); 171 $frontMatterData = $frontMatterStore->getData(); 172 173 $transfer = MetadataStoreTransfer::createForPage($parsedPage) 174 ->fromStore($frontMatterStore) 175 ->toStore($targetStore) 176 ->setMetadatas($frontMatterData) 177 ->validate(); 178 179 $messages = $transfer->getMessages(); 180 $validatedMetadatas = $transfer->getValidatedMetadatas(); 181 $renderMetadata = []; 182 foreach ($validatedMetadatas as $metadataObject) { 183 $renderMetadata[$metadataObject::getPersistentName()] = $metadataObject->toStoreValue(); 184 } 185 186 foreach ($messages as $message) { 187 $message->sendToLogUtility(); 188 } 189 190 /** 191 * Return them for metadata rendering 192 */ 193 $result[PluginUtility::ATTRIBUTES] = $renderMetadata; 194 return $result; 195 196 } 197 198 /** 199 * Render the output 200 * @param string $format 201 * @param Doku_Renderer $renderer 202 * @param array $data - what the function handle() return'ed 203 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 204 * @see DokuWiki_Syntax_Plugin::render() 205 * 206 * 207 */ 208 function render($format, Doku_Renderer $renderer, $data): bool 209 { 210 211 try { 212 $executingPath = ExecutionContext::getActualOrCreateFromEnv() 213 ->getExecutingWikiPath(); 214 } catch (ExceptionNotFound $e) { 215 // markup string rendering 216 return false; 217 } 218 switch ($format) { 219 case 'xhtml': 220 221 /** @var Doku_Renderer_xhtml $renderer */ 222 $exitCode = $data[PluginUtility::EXIT_CODE]; 223 if ($exitCode == self::PARSING_STATE_ERROR) { 224 $json = MetadataFrontmatterStore::stripFrontmatterTag($data[PluginUtility::EXIT_MESSAGE]); 225 LogUtility::error("Front Matter: The json object for the page ($executingPath) is not valid. " . \ComboStrap\Json::getValidationLink($json), self::CANONICAL); 226 } 227 return true; 228 229 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 230 231 if ($data[PluginUtility::EXIT_CODE] !== self::PARSING_STATE_SUCCESSFUL) { 232 return true; 233 } 234 235 /** @var renderer_plugin_combo_analytics $renderer */ 236 $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES]; 237 foreach ($frontMatterJsonArray as $key => $value) { 238 239 /** 240 * Hack while metadata and analtyics stats are not together 241 */ 242 if ($key === PageDescription::DESCRIPTION_PROPERTY) { 243 $value = $value['abstract']; 244 } 245 $renderer->setAnalyticsMetaForReporting($key, $value); 246 if ($key === PageImages::PROPERTY_NAME) { 247 $this->updateImageStatistics($value, $renderer); 248 } 249 250 } 251 return true; 252 253 case "metadata": 254 255 256 /** @var Doku_Renderer_metadata $renderer */ 257 if ($data[PluginUtility::EXIT_CODE] === self::PARSING_STATE_ERROR) { 258 if (PluginUtility::isTest()) { 259 // fail if test 260 throw new ExceptionRuntime("Front Matter: The json object for the page () is not valid.", LogUtility::LVL_MSG_ERROR); 261 } 262 return false; 263 } 264 265 /** 266 * Empty string 267 * Rare case, we delete all mutable meta if present 268 */ 269 $frontmatterData = $data[PluginUtility::ATTRIBUTES]; 270 if (sizeof($frontmatterData) === 0) { 271 foreach (MetadataSystem::getMutableMetadata() as $metaData) { 272 $metaKey = $metaData::getName(); 273 if ($metaKey === PageDescription::PROPERTY_NAME) { 274 // array 275 continue; 276 } 277 // runtime 278 if ($renderer->meta[$metaKey]) { 279 unset($renderer->meta[$metaKey]); 280 } 281 // persistent 282 if ($renderer->persistent[$metaKey]) { 283 unset($renderer->persistent[$metaKey]); 284 } 285 } 286 return true; 287 } 288 289 /** 290 * Meta update 291 * (The {@link p_get_metadata()} starts {@link p_render_metadata()} 292 * and stores them if there is any diff 293 */ 294 foreach ($frontmatterData as $metaKey => $metaValue) { 295 296 $renderer->meta[$metaKey] = $metaValue; 297 298 /** 299 * Persistence is just a duplicate of the meta (ie current) 300 * 301 * Why from https://www.dokuwiki.org/devel:metadata#metadata_persistence 302 * The persistent array holds ****duplicates**** 303 * as the {@link p_get_metadata()} returns only `current` data 304 * which should not be cleared during the rendering process. 305 */ 306 $renderer->persistent[$metaKey] = $metaValue; 307 308 } 309 310 311 /** 312 * Register media in index 313 */ 314 $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES]; 315 if (isset($frontMatterJsonArray[PageImages::getPersistentName()])) { 316 $value = $frontMatterJsonArray[PageImages::getPersistentName()]; 317 318 /** 319 * @var PageImages $pageImages 320 */ 321 $page = MarkupPath::createPageFromPathObject($executingPath); 322 $pageImages = PageImages::createForPage($page) 323 ->setFromStoreValueWithoutException($value); 324 $pageImagesObject = $pageImages->getValueAsPageImages(); 325 foreach ($pageImagesObject as $imageValue) { 326 $dokuwikiId = $imageValue->getImagePath()->getWikiId(); 327 $attributes = [MarkupRef::REF_ATTRIBUTE => ":$dokuwikiId"]; 328 try { 329 syntax_plugin_combo_media::registerImageMeta($attributes, $renderer); 330 } catch (\Exception $e) { 331 LogUtility::internalError("The image registration did not work. Error: {$e->getMessage()}"); 332 } 333 } 334 } 335 break; 336 337 } 338 return true; 339 } 340 341 342 private function updateImageStatistics($value, $renderer) 343 { 344 if (is_array($value) && sizeof($value) > 0) { 345 $firstKey = array_keys($value)[0]; 346 if (is_numeric($firstKey)) { 347 foreach ($value as $subImage) { 348 $this->updateImageStatistics($subImage, $renderer); 349 } 350 return; 351 } 352 } 353 354 /** 355 * Code below is fucked up 356 */ 357 $path = $value; 358 if (is_array($value) && isset($value[PageImagePath::getPersistentName()])) { 359 $path = $value[PageImagePath::getPersistentName()]; 360 } 361 try { 362 $media = MediaMarkup::createFromRef($path); 363 } catch (ExceptionBadArgument|ExceptionNotFound|ExceptionBadSyntax $e) { 364 LogUtility::internalError("The media image statistics could not be created. The media markup could not be instantiated with the path ($path). Error:{$e->getMessage()}"); 365 return; 366 } 367 368 $attributes = $media->toCallStackArray(); 369 syntax_plugin_combo_media::updateStatistics($attributes, $renderer); 370 371 } 372 373 374} 375 376