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