1007225e5Sgerardnico<?php 2007225e5Sgerardnico/** 3007225e5Sgerardnico * Front Matter implementation to add metadata 4007225e5Sgerardnico * 5007225e5Sgerardnico * 6007225e5Sgerardnico * that enhance the metadata dokuwiki system 7007225e5Sgerardnico * https://www.dokuwiki.org/metadata 8007225e5Sgerardnico * that use the Dublin Core Standard 9007225e5Sgerardnico * http://dublincore.org/ 10007225e5Sgerardnico * by adding the front matter markup specification 11007225e5Sgerardnico * https://gerardnico.com/markup/front-matter 12007225e5Sgerardnico * 13007225e5Sgerardnico * Inspiration 14007225e5Sgerardnico * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php 15007225e5Sgerardnico * https://www.dokuwiki.org/plugin:semantic 16007225e5Sgerardnico * 17007225e5Sgerardnico * See also structured plugin 18007225e5Sgerardnico * https://www.dokuwiki.org/plugin:data 19007225e5Sgerardnico * https://www.dokuwiki.org/plugin:struct 20007225e5Sgerardnico * 21007225e5Sgerardnico */ 22007225e5Sgerardnico 23007225e5Sgerardnicouse ComboStrap\Analytics; 24007225e5Sgerardnicouse ComboStrap\LogUtility; 25007225e5Sgerardnicouse ComboStrap\PluginUtility; 2671f916b9Sgerardnicouse ComboStrap\Page; 27007225e5Sgerardnico 28007225e5Sgerardnicorequire_once(__DIR__ . '/../class/Analytics.php'); 29007225e5Sgerardnico 30007225e5Sgerardnicoif (!defined('DOKU_INC')) { 31007225e5Sgerardnico die(); 32007225e5Sgerardnico} 33007225e5Sgerardnico 34007225e5Sgerardnico/** 35007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism 36007225e5Sgerardnico * need to inherit from this class 37d5303bc5Sgerardnico * 38d5303bc5Sgerardnico * For a list of meta, see also https://ghost.org/docs/publishing/#api-data 39007225e5Sgerardnico */ 40007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin 41007225e5Sgerardnico{ 42007225e5Sgerardnico const PARSING_STATE_EMPTY = "empty"; 43007225e5Sgerardnico const PARSING_STATE_ERROR = "error"; 44007225e5Sgerardnico const PARSING_STATE_SUCCESSFUL = "successful"; 455f891b7eSNickeau const STATUS = "status"; 465f891b7eSNickeau const CANONICAL = "frontmatter"; 4721913ab3SNickeau const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing'; 48007225e5Sgerardnico 49007225e5Sgerardnico /** 50007225e5Sgerardnico * Syntax Type. 51007225e5Sgerardnico * 52007225e5Sgerardnico * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 53007225e5Sgerardnico * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 54007225e5Sgerardnico * 55007225e5Sgerardnico * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 56007225e5Sgerardnico * 57007225e5Sgerardnico * baseonly - run only in the base 58007225e5Sgerardnico */ 59007225e5Sgerardnico function getType() 60007225e5Sgerardnico { 61007225e5Sgerardnico return 'baseonly'; 62007225e5Sgerardnico } 63007225e5Sgerardnico 64*531e725cSNickeau public function getPType() 65*531e725cSNickeau { 66*531e725cSNickeau return "normal"; 67*531e725cSNickeau } 68*531e725cSNickeau 69*531e725cSNickeau 70007225e5Sgerardnico /** 71007225e5Sgerardnico * @see Doku_Parser_Mode::getSort() 72007225e5Sgerardnico * Higher number than the teaser-columns 73007225e5Sgerardnico * because the mode with the lowest sort number will win out 74007225e5Sgerardnico */ 75007225e5Sgerardnico function getSort() 76007225e5Sgerardnico { 77007225e5Sgerardnico return 99; 78007225e5Sgerardnico } 79007225e5Sgerardnico 80007225e5Sgerardnico /** 81007225e5Sgerardnico * Create a pattern that will called this plugin 82007225e5Sgerardnico * 83007225e5Sgerardnico * @param string $mode 84007225e5Sgerardnico * @see Doku_Parser_Mode::connectTo() 85007225e5Sgerardnico */ 86007225e5Sgerardnico function connectTo($mode) 87007225e5Sgerardnico { 88007225e5Sgerardnico if ($mode == "base") { 89007225e5Sgerardnico // only from the top 90007225e5Sgerardnico $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent())); 91007225e5Sgerardnico } 92007225e5Sgerardnico } 93007225e5Sgerardnico 94007225e5Sgerardnico /** 95007225e5Sgerardnico * 96007225e5Sgerardnico * The handle function goal is to parse the matched syntax through the pattern function 97007225e5Sgerardnico * and to return the result for use in the renderer 98007225e5Sgerardnico * This result is always cached until the page is modified. 99007225e5Sgerardnico * @param string $match 100007225e5Sgerardnico * @param int $state 101007225e5Sgerardnico * @param int $pos 102007225e5Sgerardnico * @param Doku_Handler $handler 103007225e5Sgerardnico * @return array|bool 104007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::handle() 105007225e5Sgerardnico * 106007225e5Sgerardnico */ 107007225e5Sgerardnico function handle($match, $state, $pos, Doku_Handler $handler) 108007225e5Sgerardnico { 109007225e5Sgerardnico 110007225e5Sgerardnico if ($state == DOKU_LEXER_SPECIAL) { 111007225e5Sgerardnico 112007225e5Sgerardnico global $ID; 113007225e5Sgerardnico 114007225e5Sgerardnico // strip 115007225e5Sgerardnico // from start `---json` + eol = 8 116007225e5Sgerardnico // from end `---` + eol = 4 117*531e725cSNickeau $jsonString = substr($match, 7, -3); 118007225e5Sgerardnico 119007225e5Sgerardnico // Empty front matter 120*531e725cSNickeau if (trim($jsonString) == "") { 121007225e5Sgerardnico $this->closeParsing(); 1225f891b7eSNickeau return array(self::STATUS => self::PARSING_STATE_EMPTY); 123007225e5Sgerardnico } 124007225e5Sgerardnico 125007225e5Sgerardnico // Otherwise you get an object ie $arrayFormat-> syntax 126007225e5Sgerardnico $arrayFormat = true; 127*531e725cSNickeau $jsonArray = json_decode($jsonString, $arrayFormat); 128007225e5Sgerardnico 129007225e5Sgerardnico // Decodage problem 130*531e725cSNickeau if ($jsonArray == null) { 1315f891b7eSNickeau return array( 1325f891b7eSNickeau self::STATUS => self::PARSING_STATE_ERROR, 1335f891b7eSNickeau PluginUtility::PAYLOAD => $match 1345f891b7eSNickeau ); 135007225e5Sgerardnico } 136007225e5Sgerardnico 137007225e5Sgerardnico $notModifiableMeta = [ 138007225e5Sgerardnico "date", 139007225e5Sgerardnico "user", 140007225e5Sgerardnico "last_change", 141007225e5Sgerardnico "creator", 142007225e5Sgerardnico "contributor" 143007225e5Sgerardnico ]; 144007225e5Sgerardnico $result = array(); 145*531e725cSNickeau foreach ($jsonArray as $key => $value) { 146007225e5Sgerardnico 1475f891b7eSNickeau $lowerCaseKey = trim(strtolower($key)); 1485f891b7eSNickeau 149007225e5Sgerardnico // Not modifiable metadata 1505f891b7eSNickeau if (in_array($lowerCaseKey, $notModifiableMeta)) { 1515f891b7eSNickeau LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING); 152007225e5Sgerardnico continue; 153007225e5Sgerardnico } 154007225e5Sgerardnico 1555f891b7eSNickeau switch ($lowerCaseKey) { 1565f891b7eSNickeau 1575f891b7eSNickeau case Page::DESCRIPTION_PROPERTY: 158007225e5Sgerardnico $result["description"] = $value; 1595f891b7eSNickeau /** 1605f891b7eSNickeau * Overwrite also the actual description 1615f891b7eSNickeau */ 1625f891b7eSNickeau p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array( 1635f891b7eSNickeau "abstract" => $value, 1645f891b7eSNickeau "origin" => syntax_plugin_combo_frontmatter::CANONICAL 1655f891b7eSNickeau ))); 1665f891b7eSNickeau /** 1675f891b7eSNickeau * Continue because 1685f891b7eSNickeau * the description value was already stored 1695f891b7eSNickeau * We don't want to override it 1705f891b7eSNickeau * And continue 2 because continue == break in a switch 1715f891b7eSNickeau */ 1725f891b7eSNickeau continue 2; 1735f891b7eSNickeau break; 174007225e5Sgerardnico 175f3748b38Sgerardnico /** 1769b9e6d1fSgerardnico * Pass the title to the metadata 177f3748b38Sgerardnico * to advertise that it's in the front-matter 178f3748b38Sgerardnico * for the quality rules 179f3748b38Sgerardnico */ 1805f891b7eSNickeau case Page::TITLE_PROPERTY: 181f3748b38Sgerardnico $result[Page::TITLE_PROPERTY] = $value; 1825f891b7eSNickeau break; 183f3748b38Sgerardnico 1849b9e6d1fSgerardnico /** 1859b9e6d1fSgerardnico * Pass the low quality indicator 1869b9e6d1fSgerardnico * to advertise that it's in the front-matter 1879b9e6d1fSgerardnico */ 1885f891b7eSNickeau case Page::LOW_QUALITY_PAGE_INDICATOR: 1899b9e6d1fSgerardnico $result[Page::LOW_QUALITY_PAGE_INDICATOR] = $value; 1905f891b7eSNickeau break; 1919b9e6d1fSgerardnico 192007225e5Sgerardnico // Canonical should be lowercase 1935f891b7eSNickeau case Page::CANONICAL_PROPERTY: 19471f916b9Sgerardnico $result[Page::CANONICAL_PROPERTY] = $value; 195007225e5Sgerardnico $value = strtolower($value); 1965f891b7eSNickeau break; 197007225e5Sgerardnico 1985f891b7eSNickeau } 199007225e5Sgerardnico // Set the value persistently 2005f891b7eSNickeau p_set_metadata($ID, array($lowerCaseKey => $value)); 201007225e5Sgerardnico 202007225e5Sgerardnico } 203007225e5Sgerardnico 204*531e725cSNickeau $this->closeParsing($jsonArray); 205007225e5Sgerardnico 2065f891b7eSNickeau $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL; 207*531e725cSNickeau 208*531e725cSNickeau /** 209*531e725cSNickeau * End position is the length of the match + 1 for the newline 210*531e725cSNickeau */ 211*531e725cSNickeau $newLine = 1; 212*531e725cSNickeau $endPosition = $pos + strlen($match) + $newLine; 213*531e725cSNickeau $result[PluginUtility::POSITION]=[$pos, $endPosition]; 214007225e5Sgerardnico 215007225e5Sgerardnico return $result; 216007225e5Sgerardnico } 217007225e5Sgerardnico 218007225e5Sgerardnico return array(); 219007225e5Sgerardnico } 220007225e5Sgerardnico 221007225e5Sgerardnico /** 222007225e5Sgerardnico * Render the output 223007225e5Sgerardnico * @param string $format 224007225e5Sgerardnico * @param Doku_Renderer $renderer 225007225e5Sgerardnico * @param array $data - what the function handle() return'ed 226007225e5Sgerardnico * @return boolean - rendered correctly? (however, returned value is not used at the moment) 227007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::render() 228007225e5Sgerardnico * 229007225e5Sgerardnico * 230007225e5Sgerardnico */ 231007225e5Sgerardnico function render($format, Doku_Renderer $renderer, $data) 232007225e5Sgerardnico { 233007225e5Sgerardnico 234007225e5Sgerardnico switch ($format) { 235007225e5Sgerardnico case 'xhtml': 236007225e5Sgerardnico global $ID; 237007225e5Sgerardnico /** @var Doku_Renderer_xhtml $renderer */ 23821913ab3SNickeau 2395f891b7eSNickeau $state = $data[self::STATUS]; 240007225e5Sgerardnico if ($state == self::PARSING_STATE_ERROR) { 2415f891b7eSNickeau $json = $data[PluginUtility::PAYLOAD]; 2425f891b7eSNickeau 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); 243007225e5Sgerardnico } 24421913ab3SNickeau 24521913ab3SNickeau /** 24621913ab3SNickeau * Section 24721913ab3SNickeau */ 24821913ab3SNickeau list($startPosition,$endPosition) = $data[PluginUtility::POSITION]; 24921913ab3SNickeau if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING,1)) { 25021913ab3SNickeau $position = $startPosition; 25121913ab3SNickeau $name = self::CANONICAL; 25221913ab3SNickeau PluginUtility::startSection($renderer, $position, $name); 25321913ab3SNickeau $renderer->finishSectionEdit($endPosition); 25421913ab3SNickeau } 255007225e5Sgerardnico break; 256*531e725cSNickeau case renderer_plugin_combo_analytics::RENDERER_FORMAT: 257007225e5Sgerardnico /** @var renderer_plugin_combo_analytics $renderer */ 258007225e5Sgerardnico if (array_key_exists("description", $data)) { 259007225e5Sgerardnico $renderer->setMeta("description", $data["description"]); 260007225e5Sgerardnico } 26171f916b9Sgerardnico if (array_key_exists(Page::CANONICAL_PROPERTY, $data)) { 26271f916b9Sgerardnico $renderer->setMeta(Page::CANONICAL_PROPERTY, $data[Page::CANONICAL_PROPERTY]); 263007225e5Sgerardnico } 264f3748b38Sgerardnico if (array_key_exists(Page::TITLE_PROPERTY, $data)) { 265f3748b38Sgerardnico $renderer->setMeta(Page::TITLE_PROPERTY, $data[Page::TITLE_PROPERTY]); 266f3748b38Sgerardnico } 2679b9e6d1fSgerardnico if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $data)) { 2689b9e6d1fSgerardnico $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $data[Page::LOW_QUALITY_PAGE_INDICATOR]); 2699b9e6d1fSgerardnico } 270007225e5Sgerardnico break; 271007225e5Sgerardnico 272007225e5Sgerardnico } 273007225e5Sgerardnico return true; 274007225e5Sgerardnico } 275007225e5Sgerardnico 276007225e5Sgerardnico /** 277007225e5Sgerardnico * 278007225e5Sgerardnico * @param array $json - The Json 279007225e5Sgerardnico * Delete the controlled meta that are no more present if they exists 280007225e5Sgerardnico * @return bool 281007225e5Sgerardnico */ 2825f891b7eSNickeau public 2835f891b7eSNickeau function closeParsing(array $json = array()) 284007225e5Sgerardnico { 285007225e5Sgerardnico global $ID; 286007225e5Sgerardnico 287007225e5Sgerardnico /** 288007225e5Sgerardnico * The managed meta with the exception of 289007225e5Sgerardnico * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description} 290007225e5Sgerardnico * because it's already managed by dokuwiki in description['abstract'] 291007225e5Sgerardnico */ 292007225e5Sgerardnico $managedMeta = [ 29371f916b9Sgerardnico Page::CANONICAL_PROPERTY, 294007225e5Sgerardnico action_plugin_combo_metatitle::TITLE_META_KEY, 295007225e5Sgerardnico syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER 296007225e5Sgerardnico ]; 297007225e5Sgerardnico $meta = p_read_metadata($ID); 298007225e5Sgerardnico foreach ($managedMeta as $metaKey) { 299007225e5Sgerardnico if (!array_key_exists($metaKey, $json)) { 300007225e5Sgerardnico if (isset($meta['persistent'][$metaKey])) { 301007225e5Sgerardnico unset($meta['persistent'][$metaKey]); 302007225e5Sgerardnico } 303007225e5Sgerardnico } 304007225e5Sgerardnico } 305007225e5Sgerardnico return p_save_metadata($ID, $meta); 306007225e5Sgerardnico } 307007225e5Sgerardnico 308007225e5Sgerardnico 309007225e5Sgerardnico} 310007225e5Sgerardnico 311