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\LogUtility; 24use ComboStrap\Page; 25use ComboStrap\PluginUtility; 26 27require_once(__DIR__ . '/../class/PluginUtility.php'); 28 29if (!defined('DOKU_INC')) { 30 die(); 31} 32 33/** 34 * All DokuWiki plugins to extend the parser/rendering mechanism 35 * need to inherit from this class 36 * 37 * For a list of meta, see also https://ghost.org/docs/publishing/#api-data 38 */ 39class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin 40{ 41 const PARSING_STATE_EMPTY = "empty"; 42 const PARSING_STATE_ERROR = "error"; 43 const PARSING_STATE_SUCCESSFUL = "successful"; 44 const STATUS = "status"; 45 const CANONICAL = "frontmatter"; 46 const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing'; 47 48 /** 49 * Syntax Type. 50 * 51 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 52 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 53 * 54 * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 55 * 56 * baseonly - run only in the base 57 */ 58 function getType() 59 { 60 return 'baseonly'; 61 } 62 63 public function getPType() 64 { 65 /** 66 * This element create a section 67 * element that is a div 68 * that should not be in paragraph 69 * 70 * We make it a block 71 */ 72 return "block"; 73 } 74 75 76 /** 77 * @see Doku_Parser_Mode::getSort() 78 * Higher number than the teaser-columns 79 * because the mode with the lowest sort number will win out 80 */ 81 function getSort() 82 { 83 return 99; 84 } 85 86 /** 87 * Create a pattern that will called this plugin 88 * 89 * @param string $mode 90 * @see Doku_Parser_Mode::connectTo() 91 */ 92 function connectTo($mode) 93 { 94 if ($mode == "base") { 95 // only from the top 96 $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 97 } 98 } 99 100 /** 101 * 102 * The handle function goal is to parse the matched syntax through the pattern function 103 * and to return the result for use in the renderer 104 * This result is always cached until the page is modified. 105 * @param string $match 106 * @param int $state 107 * @param int $pos 108 * @param Doku_Handler $handler 109 * @return array|bool 110 * @see DokuWiki_Syntax_Plugin::handle() 111 * 112 */ 113 function handle($match, $state, $pos, Doku_Handler $handler) 114 { 115 116 if ($state == DOKU_LEXER_SPECIAL) { 117 118 // strip 119 // from start `---json` + eol = 8 120 // from end `---` + eol = 4 121 $jsonString = substr($match, 7, -3); 122 123 // Empty front matter 124 if (trim($jsonString) == "") { 125 $this->deleteKnownMetaThatAreNoMorePresent(); 126 return array(self::STATUS => self::PARSING_STATE_EMPTY); 127 } 128 129 // Otherwise you get an object ie $arrayFormat-> syntax 130 $arrayFormat = true; 131 $jsonArray = json_decode($jsonString, $arrayFormat); 132 133 $result = []; 134 // Decodage problem 135 if ($jsonArray == null) { 136 $result[self::STATUS] = self::PARSING_STATE_ERROR; 137 $result[PluginUtility::PAYLOAD] = $match; 138 } else { 139 $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL; 140 $result[PluginUtility::ATTRIBUTES] = $jsonArray; 141 } 142 143 /** 144 * End position is the length of the match + 1 for the newline 145 */ 146 $newLine = 1; 147 $endPosition = $pos + strlen($match) + $newLine; 148 $result[PluginUtility::POSITION] = [$pos, $endPosition]; 149 150 return $result; 151 } 152 153 return array(); 154 } 155 156 /** 157 * Render the output 158 * @param string $format 159 * @param Doku_Renderer $renderer 160 * @param array $data - what the function handle() return'ed 161 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 162 * @see DokuWiki_Syntax_Plugin::render() 163 * 164 * 165 */ 166 function render($format, Doku_Renderer $renderer, $data) 167 { 168 169 switch ($format) { 170 case 'xhtml': 171 global $ID; 172 /** @var Doku_Renderer_xhtml $renderer */ 173 174 $state = $data[self::STATUS]; 175 if ($state == self::PARSING_STATE_ERROR) { 176 $json = $data[PluginUtility::PAYLOAD]; 177 LogUtility::msg("Front Matter: The json object for the page ($ID) is not valid. See the errors it by clicking on <a href=\"https://jsonformatter.curiousconcept.com/?data=" . urlencode($json) . "\">this link</a>.", LogUtility::LVL_MSG_ERROR); 178 } 179 180 /** 181 * Section 182 */ 183 list($startPosition, $endPosition) = $data[PluginUtility::POSITION]; 184 if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) { 185 $position = $startPosition; 186 $name = self::CANONICAL; 187 PluginUtility::startSection($renderer, $position, $name); 188 $renderer->finishSectionEdit($endPosition); 189 } 190 break; 191 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 192 193 if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) { 194 return false; 195 } 196 197 /** @var renderer_plugin_combo_analytics $renderer */ 198 $jsonArray = $data[PluginUtility::ATTRIBUTES]; 199 if (array_key_exists("description", $jsonArray)) { 200 $renderer->setMeta("description", $jsonArray["description"]); 201 } 202 if (array_key_exists(Page::CANONICAL_PROPERTY, $jsonArray)) { 203 $renderer->setMeta(Page::CANONICAL_PROPERTY, $jsonArray[Page::CANONICAL_PROPERTY]); 204 } 205 if (array_key_exists(Page::TITLE_PROPERTY, $jsonArray)) { 206 $renderer->setMeta(Page::TITLE_PROPERTY, $jsonArray[Page::TITLE_PROPERTY]); 207 } 208 if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $jsonArray)) { 209 $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $jsonArray[Page::LOW_QUALITY_PAGE_INDICATOR]); 210 } 211 break; 212 case "metadata": 213 214 if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) { 215 return false; 216 } 217 218 global $ID; 219 $jsonArray = $data[PluginUtility::ATTRIBUTES]; 220 221 222 $notModifiableMeta = [ 223 "date", 224 "user", 225 "last_change", 226 "creator", 227 "contributor" 228 ]; 229 230 foreach ($jsonArray as $key => $value) { 231 232 $lowerCaseKey = trim(strtolower($key)); 233 234 // Not modifiable metadata 235 if (in_array($lowerCaseKey, $notModifiableMeta)) { 236 LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING); 237 continue; 238 } 239 240 switch ($lowerCaseKey) { 241 242 case Page::DESCRIPTION_PROPERTY: 243 /** 244 * Overwrite also the actual description 245 */ 246 p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array( 247 "abstract" => $value, 248 "origin" => syntax_plugin_combo_frontmatter::CANONICAL 249 ))); 250 /** 251 * Continue because 252 * the description value was already stored 253 * We don't want to override it 254 * And continue 2 because continue == break in a switch 255 */ 256 continue 2; 257 258 259 // Canonical should be lowercase 260 case Page::CANONICAL_PROPERTY: 261 $value = strtolower($value); 262 break; 263 264 } 265 // Set the value persistently 266 p_set_metadata($ID, array($lowerCaseKey => $value)); 267 268 } 269 270 $this->deleteKnownMetaThatAreNoMorePresent($jsonArray); 271 272 break; 273 274 } 275 return true; 276 } 277 278 /** 279 * 280 * @param array $json - The Json 281 * Delete the controlled meta that are no more present if they exists 282 * @return bool 283 */ 284 public 285 function deleteKnownMetaThatAreNoMorePresent(array $json = array()) 286 { 287 global $ID; 288 289 /** 290 * The managed meta with the exception of 291 * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description} 292 * because it's already managed by dokuwiki in description['abstract'] 293 */ 294 $managedMeta = [ 295 Page::CANONICAL_PROPERTY, 296 action_plugin_combo_metatitle::TITLE_META_KEY, 297 syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER 298 ]; 299 $meta = p_read_metadata($ID); 300 foreach ($managedMeta as $metaKey) { 301 if (!array_key_exists($metaKey, $json)) { 302 if (isset($meta['persistent'][$metaKey])) { 303 unset($meta['persistent'][$metaKey]); 304 } 305 } 306 } 307 return p_save_metadata($ID, $meta); 308 } 309 310 311} 312 313