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\Analytics; 24use ComboStrap\LogUtility; 25use ComboStrap\PluginUtility; 26use ComboStrap\Page; 27 28require_once(__DIR__ . '/../class/Analytics.php'); 29 30if (!defined('DOKU_INC')) { 31 die(); 32} 33 34/** 35 * All DokuWiki plugins to extend the parser/rendering mechanism 36 * need to inherit from this class 37 */ 38class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin 39{ 40 const PARSING_STATE_EMPTY = "empty"; 41 const PARSING_STATE_ERROR = "error"; 42 const PARSING_STATE_SUCCESSFUL = "successful"; 43 44 /** 45 * Syntax Type. 46 * 47 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 48 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 49 * 50 * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 51 * 52 * baseonly - run only in the base 53 */ 54 function getType() 55 { 56 return 'baseonly'; 57 } 58 59 /** 60 * @see Doku_Parser_Mode::getSort() 61 * Higher number than the teaser-columns 62 * because the mode with the lowest sort number will win out 63 */ 64 function getSort() 65 { 66 return 99; 67 } 68 69 /** 70 * Create a pattern that will called this plugin 71 * 72 * @param string $mode 73 * @see Doku_Parser_Mode::connectTo() 74 */ 75 function connectTo($mode) 76 { 77 if ($mode == "base") { 78 // only from the top 79 $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent())); 80 } 81 } 82 83 /** 84 * 85 * The handle function goal is to parse the matched syntax through the pattern function 86 * and to return the result for use in the renderer 87 * This result is always cached until the page is modified. 88 * @param string $match 89 * @param int $state 90 * @param int $pos 91 * @param Doku_Handler $handler 92 * @return array|bool 93 * @see DokuWiki_Syntax_Plugin::handle() 94 * 95 */ 96 function handle($match, $state, $pos, Doku_Handler $handler) 97 { 98 99 if ($state == DOKU_LEXER_SPECIAL) { 100 101 global $ID; 102 103 // strip 104 // from start `---json` + eol = 8 105 // from end `---` + eol = 4 106 $match = substr($match, 7, -3); 107 108 // Empty front matter 109 if (trim($match) == "") { 110 $this->closeParsing(); 111 return array("state" => self::PARSING_STATE_EMPTY); 112 } 113 114 // Otherwise you get an object ie $arrayFormat-> syntax 115 $arrayFormat = true; 116 $json = json_decode($match, $arrayFormat); 117 118 // Decodage problem 119 if ($json == null) { 120 return array("state" => self::PARSING_STATE_ERROR); 121 } 122 123 // Trim it 124 $jsonKey = array_map('trim', array_keys($json)); 125 // We will get a php warning here because the values may be an array 126 // and trim accept only string 127 $oldLevel = error_reporting(E_ERROR); 128 $jsonValues = array_map('trim', $json); 129 error_reporting($oldLevel); 130 $json = array_combine($jsonKey, $jsonValues); 131 132 133 $notModifiableMeta = [ 134 "date", 135 "user", 136 "last_change", 137 "creator", 138 "contributor" 139 ]; 140 $result = array(); 141 foreach ($json as $key => $value) { 142 143 // Not modifiable metadata 144 if (in_array($key, $notModifiableMeta)) { 145 LogUtility::msg("Front Matter: The metadata ($key) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING); 146 continue; 147 } 148 149 // Description is special 150 if ($key == "description") { 151 $result["description"] = $value; 152 p_set_metadata($ID, array("description" => array("abstract" => $value))); 153 continue; 154 } 155 156 /** 157 * Pass the title to the metadata 158 * to advertise that it's in the front-matter 159 * for the quality rules 160 */ 161 if ($key == Page::TITLE_PROPERTY) { 162 $result[Page::TITLE_PROPERTY] = $value; 163 } 164 165 /** 166 * Pass the low quality indicator 167 * to advertise that it's in the front-matter 168 */ 169 if ($key == Page::LOW_QUALITY_PAGE_INDICATOR) { 170 $result[Page::LOW_QUALITY_PAGE_INDICATOR] = $value; 171 } 172 173 // Canonical should be lowercase 174 if ($key == Page::CANONICAL_PROPERTY) { 175 $result[Page::CANONICAL_PROPERTY] = $value; 176 $value = strtolower($value); 177 } 178 179 // Set the value persistently 180 p_set_metadata($ID, array($key => $value)); 181 182 } 183 184 $this->closeParsing($json); 185 186 $result["state"]= self::PARSING_STATE_SUCCESSFUL; 187 188 return $result; 189 } 190 191 return array(); 192 } 193 194 /** 195 * Render the output 196 * @param string $format 197 * @param Doku_Renderer $renderer 198 * @param array $data - what the function handle() return'ed 199 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 200 * @see DokuWiki_Syntax_Plugin::render() 201 * 202 * 203 */ 204 function render($format, Doku_Renderer $renderer, $data) 205 { 206 // TODO: https://developers.google.com/search/docs/data-types/breadcrumb#breadcrumb-list 207 // News article: https://developers.google.com/search/docs/data-types/article 208 // News article: https://developers.google.com/search/docs/data-types/paywalled-content 209 // What is ?: https://developers.google.com/search/docs/data-types/qapage 210 // How to ?: https://developers.google.com/search/docs/data-types/how-to 211 212 switch ($format) { 213 case 'xhtml': 214 global $ID; 215 /** @var Doku_Renderer_xhtml $renderer */ 216 $state = $data["state"]; 217 if ($state == self::PARSING_STATE_ERROR) { 218 LogUtility::msg("Front Matter: The json object for the page ($ID) is not valid", LogUtility::LVL_MSG_ERROR); 219 } 220 break; 221 case Analytics::RENDERER_FORMAT: 222 /** @var renderer_plugin_combo_analytics $renderer */ 223 if (array_key_exists("description", $data)) { 224 $renderer->setMeta("description", $data["description"]); 225 } 226 if (array_key_exists(Page::CANONICAL_PROPERTY, $data)) { 227 $renderer->setMeta(Page::CANONICAL_PROPERTY, $data[Page::CANONICAL_PROPERTY]); 228 } 229 if (array_key_exists(Page::TITLE_PROPERTY, $data)) { 230 $renderer->setMeta(Page::TITLE_PROPERTY, $data[Page::TITLE_PROPERTY]); 231 } 232 if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $data)) { 233 $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $data[Page::LOW_QUALITY_PAGE_INDICATOR]); 234 } 235 break; 236 237 } 238 return true; 239 } 240 241 /** 242 * 243 * @param array $json - The Json 244 * Delete the controlled meta that are no more present if they exists 245 * @return bool 246 */ 247 public function closeParsing(array $json = array()) 248 { 249 global $ID; 250 251 /** 252 * The managed meta with the exception of 253 * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description} 254 * because it's already managed by dokuwiki in description['abstract'] 255 */ 256 $managedMeta = [ 257 Page::CANONICAL_PROPERTY, 258 action_plugin_combo_metatitle::TITLE_META_KEY, 259 syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER 260 ]; 261 $meta = p_read_metadata($ID); 262 foreach ($managedMeta as $metaKey) { 263 if (!array_key_exists($metaKey, $json)) { 264 if (isset($meta['persistent'][$metaKey])) { 265 unset($meta['persistent'][$metaKey]); 266 } 267 } 268 } 269 return p_save_metadata($ID, $meta); 270 } 271 272 273} 274 275