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 const STATUS = "status"; 46 const CANONICAL = "frontmatter"; 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 /** 64 * @see Doku_Parser_Mode::getSort() 65 * Higher number than the teaser-columns 66 * because the mode with the lowest sort number will win out 67 */ 68 function getSort() 69 { 70 return 99; 71 } 72 73 /** 74 * Create a pattern that will called this plugin 75 * 76 * @param string $mode 77 * @see Doku_Parser_Mode::connectTo() 78 */ 79 function connectTo($mode) 80 { 81 if ($mode == "base") { 82 // only from the top 83 $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent())); 84 } 85 } 86 87 /** 88 * 89 * The handle function goal is to parse the matched syntax through the pattern function 90 * and to return the result for use in the renderer 91 * This result is always cached until the page is modified. 92 * @param string $match 93 * @param int $state 94 * @param int $pos 95 * @param Doku_Handler $handler 96 * @return array|bool 97 * @see DokuWiki_Syntax_Plugin::handle() 98 * 99 */ 100 function handle($match, $state, $pos, Doku_Handler $handler) 101 { 102 103 if ($state == DOKU_LEXER_SPECIAL) { 104 105 global $ID; 106 107 // strip 108 // from start `---json` + eol = 8 109 // from end `---` + eol = 4 110 $match = substr($match, 7, -3); 111 112 // Empty front matter 113 if (trim($match) == "") { 114 $this->closeParsing(); 115 return array(self::STATUS => self::PARSING_STATE_EMPTY); 116 } 117 118 // Otherwise you get an object ie $arrayFormat-> syntax 119 $arrayFormat = true; 120 $json = json_decode($match, $arrayFormat); 121 122 // Decodage problem 123 if ($json == null) { 124 return array( 125 self::STATUS => self::PARSING_STATE_ERROR, 126 PluginUtility::PAYLOAD => $match 127 ); 128 } 129 130 $notModifiableMeta = [ 131 "date", 132 "user", 133 "last_change", 134 "creator", 135 "contributor" 136 ]; 137 $result = array(); 138 foreach ($json as $key => $value) { 139 140 $lowerCaseKey = trim(strtolower($key)); 141 142 // Not modifiable metadata 143 if (in_array($lowerCaseKey, $notModifiableMeta)) { 144 LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING); 145 continue; 146 } 147 148 switch ($lowerCaseKey) { 149 150 case Page::DESCRIPTION_PROPERTY: 151 $result["description"] = $value; 152 /** 153 * Overwrite also the actual description 154 */ 155 p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array( 156 "abstract" => $value, 157 "origin" => syntax_plugin_combo_frontmatter::CANONICAL 158 ))); 159 /** 160 * Continue because 161 * the description value was already stored 162 * We don't want to override it 163 * And continue 2 because continue == break in a switch 164 */ 165 continue 2; 166 break; 167 168 /** 169 * Pass the title to the metadata 170 * to advertise that it's in the front-matter 171 * for the quality rules 172 */ 173 case Page::TITLE_PROPERTY: 174 $result[Page::TITLE_PROPERTY] = $value; 175 break; 176 177 /** 178 * Pass the low quality indicator 179 * to advertise that it's in the front-matter 180 */ 181 case Page::LOW_QUALITY_PAGE_INDICATOR: 182 $result[Page::LOW_QUALITY_PAGE_INDICATOR] = $value; 183 break; 184 185 // Canonical should be lowercase 186 case Page::CANONICAL_PROPERTY: 187 $result[Page::CANONICAL_PROPERTY] = $value; 188 $value = strtolower($value); 189 break; 190 191 } 192 // Set the value persistently 193 p_set_metadata($ID, array($lowerCaseKey => $value)); 194 195 } 196 197 $this->closeParsing($json); 198 199 $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL; 200 201 return $result; 202 } 203 204 return array(); 205 } 206 207 /** 208 * Render the output 209 * @param string $format 210 * @param Doku_Renderer $renderer 211 * @param array $data - what the function handle() return'ed 212 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 213 * @see DokuWiki_Syntax_Plugin::render() 214 * 215 * 216 */ 217 function render($format, Doku_Renderer $renderer, $data) 218 { 219 220 switch ($format) { 221 case 'xhtml': 222 global $ID; 223 /** @var Doku_Renderer_xhtml $renderer */ 224 $state = $data[self::STATUS]; 225 if ($state == self::PARSING_STATE_ERROR) { 226 $json = $data[PluginUtility::PAYLOAD]; 227 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); 228 } 229 break; 230 case Analytics::RENDERER_FORMAT: 231 /** @var renderer_plugin_combo_analytics $renderer */ 232 if (array_key_exists("description", $data)) { 233 $renderer->setMeta("description", $data["description"]); 234 } 235 if (array_key_exists(Page::CANONICAL_PROPERTY, $data)) { 236 $renderer->setMeta(Page::CANONICAL_PROPERTY, $data[Page::CANONICAL_PROPERTY]); 237 } 238 if (array_key_exists(Page::TITLE_PROPERTY, $data)) { 239 $renderer->setMeta(Page::TITLE_PROPERTY, $data[Page::TITLE_PROPERTY]); 240 } 241 if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $data)) { 242 $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $data[Page::LOW_QUALITY_PAGE_INDICATOR]); 243 } 244 break; 245 246 } 247 return true; 248 } 249 250 /** 251 * 252 * @param array $json - The Json 253 * Delete the controlled meta that are no more present if they exists 254 * @return bool 255 */ 256 public 257 function closeParsing(array $json = array()) 258 { 259 global $ID; 260 261 /** 262 * The managed meta with the exception of 263 * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description} 264 * because it's already managed by dokuwiki in description['abstract'] 265 */ 266 $managedMeta = [ 267 Page::CANONICAL_PROPERTY, 268 action_plugin_combo_metatitle::TITLE_META_KEY, 269 syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER 270 ]; 271 $meta = p_read_metadata($ID); 272 foreach ($managedMeta as $metaKey) { 273 if (!array_key_exists($metaKey, $json)) { 274 if (isset($meta['persistent'][$metaKey])) { 275 unset($meta['persistent'][$metaKey]); 276 } 277 } 278 } 279 return p_save_metadata($ID, $meta); 280 } 281 282 283} 284 285