xref: /plugin/combo/syntax/frontmatter.php (revision 37748cd8654635afbeca80942126742f0f4cc346)
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
23*37748cd8SNickeauuse ComboStrap\Analytics;
24*37748cd8SNickeauuse ComboStrap\CacheManager;
25*37748cd8SNickeauuse ComboStrap\Iso8601Date;
26007225e5Sgerardnicouse ComboStrap\LogUtility;
27*37748cd8SNickeauuse ComboStrap\MediaLink;
2871f916b9Sgerardnicouse ComboStrap\Page;
29a6bf47aaSNickeauuse ComboStrap\PluginUtility;
30*37748cd8SNickeauuse ComboStrap\Publication;
31007225e5Sgerardnico
32*37748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
33007225e5Sgerardnico
34007225e5Sgerardnicoif (!defined('DOKU_INC')) {
35007225e5Sgerardnico    die();
36007225e5Sgerardnico}
37007225e5Sgerardnico
38007225e5Sgerardnico/**
39007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
40007225e5Sgerardnico * need to inherit from this class
41d5303bc5Sgerardnico *
42d5303bc5Sgerardnico * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
43007225e5Sgerardnico */
44007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
45007225e5Sgerardnico{
46007225e5Sgerardnico    const PARSING_STATE_EMPTY = "empty";
47007225e5Sgerardnico    const PARSING_STATE_ERROR = "error";
48007225e5Sgerardnico    const PARSING_STATE_SUCCESSFUL = "successful";
495f891b7eSNickeau    const STATUS = "status";
505f891b7eSNickeau    const CANONICAL = "frontmatter";
5121913ab3SNickeau    const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing';
52007225e5Sgerardnico
53007225e5Sgerardnico    /**
54*37748cd8SNickeau     * Used in the move plugin
55*37748cd8SNickeau     * !!! The two last word of the plugin class !!!
56*37748cd8SNickeau     */
57*37748cd8SNickeau    const COMPONENT = 'combo_' . self::CANONICAL;
58*37748cd8SNickeau    const START_TAG = '---json';
59*37748cd8SNickeau    const END_TAG = '---';
60*37748cd8SNickeau    const METADATA_IMAGE_CANONICAL = "metadata:image";
61*37748cd8SNickeau
62*37748cd8SNickeau    /**
63*37748cd8SNickeau     * @param $match
64*37748cd8SNickeau     * @return array|mixed - null if decodage problem, empty array if no json or an associative array
65*37748cd8SNickeau     */
66*37748cd8SNickeau    public static function FrontMatterMatchToAssociativeArray($match)
67*37748cd8SNickeau    {
68*37748cd8SNickeau        // strip
69*37748cd8SNickeau        //   from start `---json` + eol = 8
70*37748cd8SNickeau        //   from end   `---` + eol = 4
71*37748cd8SNickeau        $jsonString = substr($match, 7, -3);
72*37748cd8SNickeau
73*37748cd8SNickeau        // Empty front matter
74*37748cd8SNickeau        if (trim($jsonString) == "") {
75*37748cd8SNickeau            self::deleteKnownMetaThatAreNoMorePresent();
76*37748cd8SNickeau            return [];
77*37748cd8SNickeau        }
78*37748cd8SNickeau
79*37748cd8SNickeau        // Otherwise you get an object ie $arrayFormat-> syntax
80*37748cd8SNickeau        $arrayFormat = true;
81*37748cd8SNickeau        return json_decode($jsonString, $arrayFormat);
82*37748cd8SNickeau    }
83*37748cd8SNickeau
84*37748cd8SNickeau    /**
85007225e5Sgerardnico     * Syntax Type.
86007225e5Sgerardnico     *
87007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
88007225e5Sgerardnico     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
89007225e5Sgerardnico     *
90007225e5Sgerardnico     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
91007225e5Sgerardnico     *
92007225e5Sgerardnico     * baseonly - run only in the base
93007225e5Sgerardnico     */
94007225e5Sgerardnico    function getType()
95007225e5Sgerardnico    {
96007225e5Sgerardnico        return 'baseonly';
97007225e5Sgerardnico    }
98007225e5Sgerardnico
99531e725cSNickeau    public function getPType()
100531e725cSNickeau    {
10185e82846SNickeau        /**
10285e82846SNickeau         * This element create a section
10385e82846SNickeau         * element that is a div
10485e82846SNickeau         * that should not be in paragraph
10585e82846SNickeau         *
10685e82846SNickeau         * We make it a block
10785e82846SNickeau         */
10885e82846SNickeau        return "block";
109531e725cSNickeau    }
110531e725cSNickeau
111531e725cSNickeau
112007225e5Sgerardnico    /**
113007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
114007225e5Sgerardnico     * Higher number than the teaser-columns
115007225e5Sgerardnico     * because the mode with the lowest sort number will win out
116007225e5Sgerardnico     */
117007225e5Sgerardnico    function getSort()
118007225e5Sgerardnico    {
119007225e5Sgerardnico        return 99;
120007225e5Sgerardnico    }
121007225e5Sgerardnico
122007225e5Sgerardnico    /**
123007225e5Sgerardnico     * Create a pattern that will called this plugin
124007225e5Sgerardnico     *
125007225e5Sgerardnico     * @param string $mode
126007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
127007225e5Sgerardnico     */
128007225e5Sgerardnico    function connectTo($mode)
129007225e5Sgerardnico    {
130007225e5Sgerardnico        if ($mode == "base") {
131007225e5Sgerardnico            // only from the top
132*37748cd8SNickeau            $this->Lexer->addSpecialPattern(self::START_TAG . '.*?' . self::END_TAG, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
133007225e5Sgerardnico        }
134007225e5Sgerardnico    }
135007225e5Sgerardnico
136007225e5Sgerardnico    /**
137007225e5Sgerardnico     *
138007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
139007225e5Sgerardnico     * and to return the result for use in the renderer
140007225e5Sgerardnico     * This result is always cached until the page is modified.
141007225e5Sgerardnico     * @param string $match
142007225e5Sgerardnico     * @param int $state
143007225e5Sgerardnico     * @param int $pos
144007225e5Sgerardnico     * @param Doku_Handler $handler
145007225e5Sgerardnico     * @return array|bool
146007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
147007225e5Sgerardnico     *
148007225e5Sgerardnico     */
149007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
150007225e5Sgerardnico    {
151007225e5Sgerardnico
152007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
153007225e5Sgerardnico
154007225e5Sgerardnico
155*37748cd8SNickeau            $jsonArray = self::FrontMatterMatchToAssociativeArray($match);
156007225e5Sgerardnico
157007225e5Sgerardnico
158a6bf47aaSNickeau            $result = [];
159007225e5Sgerardnico            // Decodage problem
160531e725cSNickeau            if ($jsonArray == null) {
161*37748cd8SNickeau
162a6bf47aaSNickeau                $result[self::STATUS] = self::PARSING_STATE_ERROR;
163a6bf47aaSNickeau                $result[PluginUtility::PAYLOAD] = $match;
164*37748cd8SNickeau
165a6bf47aaSNickeau            } else {
166*37748cd8SNickeau
167*37748cd8SNickeau                if (sizeof($jsonArray) === 0) {
168*37748cd8SNickeau                    return array(self::STATUS => self::PARSING_STATE_EMPTY);
169*37748cd8SNickeau                }
170*37748cd8SNickeau
1715f891b7eSNickeau                $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
172*37748cd8SNickeau                /**
173*37748cd8SNickeau                 * Published is an alias for date published
174*37748cd8SNickeau                 */
175*37748cd8SNickeau                if (isset($jsonArray[Publication::OLD_META_KEY])) {
176*37748cd8SNickeau                    $jsonArray[Publication::DATE_PUBLISHED] = $jsonArray[Publication::OLD_META_KEY];
177*37748cd8SNickeau                    unset($jsonArray[Publication::OLD_META_KEY]);
178*37748cd8SNickeau                }
179*37748cd8SNickeau                /**
180*37748cd8SNickeau                 * Add the time part if not present
181*37748cd8SNickeau                 */
182*37748cd8SNickeau                if (isset($jsonArray[Publication::DATE_PUBLISHED])) {
183*37748cd8SNickeau                    $datePublishedString = $jsonArray[Publication::DATE_PUBLISHED];
184*37748cd8SNickeau                    $datePublished = Iso8601Date::create($datePublishedString);
185*37748cd8SNickeau                    if (!$datePublished->isValidDateEntry()) {
186*37748cd8SNickeau                        LogUtility::msg("The published date ($datePublishedString) is not a valid ISO date supported.", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
187*37748cd8SNickeau                        unset($jsonArray[Publication::DATE_PUBLISHED]);
188*37748cd8SNickeau                    } else {
189*37748cd8SNickeau                        $jsonArray[Publication::DATE_PUBLISHED] = "$datePublished";
190*37748cd8SNickeau                    }
191*37748cd8SNickeau
192*37748cd8SNickeau                }
193a6bf47aaSNickeau                $result[PluginUtility::ATTRIBUTES] = $jsonArray;
194a6bf47aaSNickeau            }
195531e725cSNickeau
196531e725cSNickeau            /**
197531e725cSNickeau             * End position is the length of the match + 1 for the newline
198531e725cSNickeau             */
199531e725cSNickeau            $newLine = 1;
200531e725cSNickeau            $endPosition = $pos + strlen($match) + $newLine;
201531e725cSNickeau            $result[PluginUtility::POSITION] = [$pos, $endPosition];
202007225e5Sgerardnico
203007225e5Sgerardnico            return $result;
204007225e5Sgerardnico        }
205007225e5Sgerardnico
206007225e5Sgerardnico        return array();
207007225e5Sgerardnico    }
208007225e5Sgerardnico
209007225e5Sgerardnico    /**
210007225e5Sgerardnico     * Render the output
211007225e5Sgerardnico     * @param string $format
212007225e5Sgerardnico     * @param Doku_Renderer $renderer
213007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
214007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
215007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
216007225e5Sgerardnico     *
217007225e5Sgerardnico     *
218007225e5Sgerardnico     */
219007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
220007225e5Sgerardnico    {
221007225e5Sgerardnico
222007225e5Sgerardnico        switch ($format) {
223007225e5Sgerardnico            case 'xhtml':
224007225e5Sgerardnico                global $ID;
225007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
22621913ab3SNickeau
2275f891b7eSNickeau                $state = $data[self::STATUS];
228007225e5Sgerardnico                if ($state == self::PARSING_STATE_ERROR) {
2295f891b7eSNickeau                    $json = $data[PluginUtility::PAYLOAD];
2305f891b7eSNickeau                    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);
231007225e5Sgerardnico                }
23221913ab3SNickeau
23321913ab3SNickeau                /**
23421913ab3SNickeau                 * Section
23521913ab3SNickeau                 */
23621913ab3SNickeau                list($startPosition, $endPosition) = $data[PluginUtility::POSITION];
23721913ab3SNickeau                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
23821913ab3SNickeau                    $position = $startPosition;
23921913ab3SNickeau                    $name = self::CANONICAL;
24021913ab3SNickeau                    PluginUtility::startSection($renderer, $position, $name);
24121913ab3SNickeau                    $renderer->finishSectionEdit($endPosition);
24221913ab3SNickeau                }
243007225e5Sgerardnico                break;
244531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
245a6bf47aaSNickeau
246a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
247a6bf47aaSNickeau                    return false;
248a6bf47aaSNickeau                }
249a6bf47aaSNickeau
250*37748cd8SNickeau                $notModifiableMeta = [
251*37748cd8SNickeau                    Analytics::PATH,
252*37748cd8SNickeau                    Analytics::DATE_CREATED,
253*37748cd8SNickeau                    Analytics::DATE_MODIFIED
254*37748cd8SNickeau                ];
255*37748cd8SNickeau
256007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
257a6bf47aaSNickeau                $jsonArray = $data[PluginUtility::ATTRIBUTES];
258*37748cd8SNickeau                foreach ($jsonArray as $key => $value) {
259*37748cd8SNickeau                    if (!in_array($key, $notModifiableMeta)) {
260*37748cd8SNickeau
261*37748cd8SNickeau                        $renderer->setMeta($key, $value);
262*37748cd8SNickeau                        if ($key === Page::IMAGE_META_PROPERTY) {
263*37748cd8SNickeau                            $this->updateImageStatistics($value, $renderer);
264007225e5Sgerardnico                        }
265*37748cd8SNickeau
266*37748cd8SNickeau                    } else {
267*37748cd8SNickeau                        LogUtility::msg("The metadata ($key) cannot be set.", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
268007225e5Sgerardnico                    }
2699b9e6d1fSgerardnico                }
270007225e5Sgerardnico                break;
271*37748cd8SNickeau
272a6bf47aaSNickeau            case "metadata":
273a6bf47aaSNickeau
274*37748cd8SNickeau                /** @var Doku_Renderer_metadata $renderer */
275a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
276a6bf47aaSNickeau                    return false;
277a6bf47aaSNickeau                }
278a6bf47aaSNickeau
279a6bf47aaSNickeau                global $ID;
280a6bf47aaSNickeau                $jsonArray = $data[PluginUtility::ATTRIBUTES];
281a6bf47aaSNickeau
282a6bf47aaSNickeau
283a6bf47aaSNickeau                $notModifiableMeta = [
284a6bf47aaSNickeau                    "date",
285a6bf47aaSNickeau                    "user",
286a6bf47aaSNickeau                    "last_change",
287a6bf47aaSNickeau                    "creator",
288a6bf47aaSNickeau                    "contributor"
289a6bf47aaSNickeau                ];
290a6bf47aaSNickeau
291a6bf47aaSNickeau                foreach ($jsonArray as $key => $value) {
292a6bf47aaSNickeau
293a6bf47aaSNickeau                    $lowerCaseKey = trim(strtolower($key));
294a6bf47aaSNickeau
295a6bf47aaSNickeau                    // Not modifiable metadata
296a6bf47aaSNickeau                    if (in_array($lowerCaseKey, $notModifiableMeta)) {
297a6bf47aaSNickeau                        LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
298a6bf47aaSNickeau                        continue;
299a6bf47aaSNickeau                    }
300a6bf47aaSNickeau
301a6bf47aaSNickeau                    switch ($lowerCaseKey) {
302a6bf47aaSNickeau
303a6bf47aaSNickeau                        case Page::DESCRIPTION_PROPERTY:
304a6bf47aaSNickeau                            /**
305a6bf47aaSNickeau                             * Overwrite also the actual description
306a6bf47aaSNickeau                             */
307a6bf47aaSNickeau                            p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
308a6bf47aaSNickeau                                "abstract" => $value,
309a6bf47aaSNickeau                                "origin" => syntax_plugin_combo_frontmatter::CANONICAL
310a6bf47aaSNickeau                            )));
311a6bf47aaSNickeau                            /**
312a6bf47aaSNickeau                             * Continue because
313a6bf47aaSNickeau                             * the description value was already stored
314a6bf47aaSNickeau                             * We don't want to override it
315a6bf47aaSNickeau                             * And continue 2 because continue == break in a switch
316a6bf47aaSNickeau                             */
317a6bf47aaSNickeau                            continue 2;
318a6bf47aaSNickeau
319a6bf47aaSNickeau
320a6bf47aaSNickeau                        // Canonical should be lowercase
321a6bf47aaSNickeau                        case Page::CANONICAL_PROPERTY:
322a6bf47aaSNickeau                            $value = strtolower($value);
323a6bf47aaSNickeau                            break;
324a6bf47aaSNickeau
325*37748cd8SNickeau                        case Page::IMAGE_META_PROPERTY:
326*37748cd8SNickeau
327*37748cd8SNickeau                            $imageValues = [];
328*37748cd8SNickeau                            $this->aggregateImageValues($imageValues, $value);
329*37748cd8SNickeau                            foreach ($imageValues as $imageValue) {
330*37748cd8SNickeau                                $media = MediaLink::createFromRenderMatch($imageValue);
331*37748cd8SNickeau                                $attributes = $media->toCallStackArray();
332*37748cd8SNickeau                                syntax_plugin_combo_media::registerImageMeta($attributes, $renderer);
333*37748cd8SNickeau                            }
334*37748cd8SNickeau                            break;
335*37748cd8SNickeau
336a6bf47aaSNickeau                    }
337a6bf47aaSNickeau                    // Set the value persistently
338a6bf47aaSNickeau                    p_set_metadata($ID, array($lowerCaseKey => $value));
339a6bf47aaSNickeau
340a6bf47aaSNickeau                }
341a6bf47aaSNickeau
342a6bf47aaSNickeau                $this->deleteKnownMetaThatAreNoMorePresent($jsonArray);
343a6bf47aaSNickeau
344a6bf47aaSNickeau                break;
345007225e5Sgerardnico
346007225e5Sgerardnico        }
347007225e5Sgerardnico        return true;
348007225e5Sgerardnico    }
349007225e5Sgerardnico
350007225e5Sgerardnico    /**
351007225e5Sgerardnico     *
352007225e5Sgerardnico     * @param array $json - The Json
353007225e5Sgerardnico     * Delete the controlled meta that are no more present if they exists
354007225e5Sgerardnico     * @return bool
355007225e5Sgerardnico     */
356*37748cd8SNickeau    static public
357a6bf47aaSNickeau    function deleteKnownMetaThatAreNoMorePresent(array $json = array())
358007225e5Sgerardnico    {
359007225e5Sgerardnico        global $ID;
360007225e5Sgerardnico
361007225e5Sgerardnico        /**
362007225e5Sgerardnico         * The managed meta with the exception of
363007225e5Sgerardnico         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
364007225e5Sgerardnico         * because it's already managed by dokuwiki in description['abstract']
365007225e5Sgerardnico         */
366007225e5Sgerardnico        $managedMeta = [
36771f916b9Sgerardnico            Page::CANONICAL_PROPERTY,
368*37748cd8SNickeau            Page::TYPE_META_PROPERTY,
369*37748cd8SNickeau            Page::IMAGE_META_PROPERTY,
370*37748cd8SNickeau            Page::COUNTRY_META_PROPERTY,
371*37748cd8SNickeau            Page::LANG_META_PROPERTY,
372*37748cd8SNickeau            Analytics::TITLE,
373*37748cd8SNickeau            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER,
374*37748cd8SNickeau            Publication::OLD_META_KEY,
375*37748cd8SNickeau            Publication::DATE_PUBLISHED,
376*37748cd8SNickeau            Analytics::NAME,
377*37748cd8SNickeau            CacheManager::DATE_CACHE_EXPIRATION_META_KEY,
378*37748cd8SNickeau            action_plugin_combo_metagoogle::JSON_LD_META_PROPERTY,
379*37748cd8SNickeau
380007225e5Sgerardnico        ];
381007225e5Sgerardnico        $meta = p_read_metadata($ID);
382007225e5Sgerardnico        foreach ($managedMeta as $metaKey) {
383007225e5Sgerardnico            if (!array_key_exists($metaKey, $json)) {
384007225e5Sgerardnico                if (isset($meta['persistent'][$metaKey])) {
385007225e5Sgerardnico                    unset($meta['persistent'][$metaKey]);
386007225e5Sgerardnico                }
387007225e5Sgerardnico            }
388007225e5Sgerardnico        }
389007225e5Sgerardnico        return p_save_metadata($ID, $meta);
390007225e5Sgerardnico    }
391007225e5Sgerardnico
392*37748cd8SNickeau    private function updateImageStatistics($value, $renderer)
393*37748cd8SNickeau    {
394*37748cd8SNickeau        if(is_array($value)){
395*37748cd8SNickeau            foreach($value as $subImage){
396*37748cd8SNickeau                $this->updateImageStatistics($subImage, $renderer);
397*37748cd8SNickeau            }
398*37748cd8SNickeau        } else {
399*37748cd8SNickeau            $media = MediaLink::createFromRenderMatch($value);
400*37748cd8SNickeau            $attributes = $media->toCallStackArray();
401*37748cd8SNickeau            syntax_plugin_combo_media::updateStatistics($attributes, $renderer);
402*37748cd8SNickeau        }
403*37748cd8SNickeau    }
404*37748cd8SNickeau
405*37748cd8SNickeau    private function aggregateImageValues(array &$imageValues, $value)
406*37748cd8SNickeau    {
407*37748cd8SNickeau        if (is_array($value)) {
408*37748cd8SNickeau            foreach ($value as $subImageValue) {
409*37748cd8SNickeau                $this->aggregateImageValues($imageValues,$subImageValue);
410*37748cd8SNickeau            }
411*37748cd8SNickeau        } else {
412*37748cd8SNickeau            $imageValues[] = $value;
413*37748cd8SNickeau        }
414*37748cd8SNickeau    }
415*37748cd8SNickeau
416007225e5Sgerardnico
417007225e5Sgerardnico}
418007225e5Sgerardnico
419