xref: /plugin/combo/syntax/frontmatter.php (revision 85e82846b0a214bc35e62864fa49d9cad0723d0e)
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\LogUtility;
2471f916b9Sgerardnicouse ComboStrap\Page;
25a6bf47aaSNickeauuse ComboStrap\PluginUtility;
26007225e5Sgerardnico
27a6bf47aaSNickeaurequire_once(__DIR__ . '/../class/PluginUtility.php');
28007225e5Sgerardnico
29007225e5Sgerardnicoif (!defined('DOKU_INC')) {
30007225e5Sgerardnico    die();
31007225e5Sgerardnico}
32007225e5Sgerardnico
33007225e5Sgerardnico/**
34007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
35007225e5Sgerardnico * need to inherit from this class
36d5303bc5Sgerardnico *
37d5303bc5Sgerardnico * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
38007225e5Sgerardnico */
39007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
40007225e5Sgerardnico{
41007225e5Sgerardnico    const PARSING_STATE_EMPTY = "empty";
42007225e5Sgerardnico    const PARSING_STATE_ERROR = "error";
43007225e5Sgerardnico    const PARSING_STATE_SUCCESSFUL = "successful";
445f891b7eSNickeau    const STATUS = "status";
455f891b7eSNickeau    const CANONICAL = "frontmatter";
4621913ab3SNickeau    const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing';
47007225e5Sgerardnico
48007225e5Sgerardnico    /**
49007225e5Sgerardnico     * Syntax Type.
50007225e5Sgerardnico     *
51007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
52007225e5Sgerardnico     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
53007225e5Sgerardnico     *
54007225e5Sgerardnico     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
55007225e5Sgerardnico     *
56007225e5Sgerardnico     * baseonly - run only in the base
57007225e5Sgerardnico     */
58007225e5Sgerardnico    function getType()
59007225e5Sgerardnico    {
60007225e5Sgerardnico        return 'baseonly';
61007225e5Sgerardnico    }
62007225e5Sgerardnico
63531e725cSNickeau    public function getPType()
64531e725cSNickeau    {
65*85e82846SNickeau        /**
66*85e82846SNickeau         * This element create a section
67*85e82846SNickeau         * element that is a div
68*85e82846SNickeau         * that should not be in paragraph
69*85e82846SNickeau         *
70*85e82846SNickeau         * We make it a block
71*85e82846SNickeau         */
72*85e82846SNickeau        return "block";
73531e725cSNickeau    }
74531e725cSNickeau
75531e725cSNickeau
76007225e5Sgerardnico    /**
77007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
78007225e5Sgerardnico     * Higher number than the teaser-columns
79007225e5Sgerardnico     * because the mode with the lowest sort number will win out
80007225e5Sgerardnico     */
81007225e5Sgerardnico    function getSort()
82007225e5Sgerardnico    {
83007225e5Sgerardnico        return 99;
84007225e5Sgerardnico    }
85007225e5Sgerardnico
86007225e5Sgerardnico    /**
87007225e5Sgerardnico     * Create a pattern that will called this plugin
88007225e5Sgerardnico     *
89007225e5Sgerardnico     * @param string $mode
90007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
91007225e5Sgerardnico     */
92007225e5Sgerardnico    function connectTo($mode)
93007225e5Sgerardnico    {
94007225e5Sgerardnico        if ($mode == "base") {
95007225e5Sgerardnico            // only from the top
969337a630SNickeau            $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
97007225e5Sgerardnico        }
98007225e5Sgerardnico    }
99007225e5Sgerardnico
100007225e5Sgerardnico    /**
101007225e5Sgerardnico     *
102007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
103007225e5Sgerardnico     * and to return the result for use in the renderer
104007225e5Sgerardnico     * This result is always cached until the page is modified.
105007225e5Sgerardnico     * @param string $match
106007225e5Sgerardnico     * @param int $state
107007225e5Sgerardnico     * @param int $pos
108007225e5Sgerardnico     * @param Doku_Handler $handler
109007225e5Sgerardnico     * @return array|bool
110007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
111007225e5Sgerardnico     *
112007225e5Sgerardnico     */
113007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
114007225e5Sgerardnico    {
115007225e5Sgerardnico
116007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
117007225e5Sgerardnico
118007225e5Sgerardnico            // strip
119007225e5Sgerardnico            //   from start `---json` + eol = 8
120007225e5Sgerardnico            //   from end   `---` + eol = 4
121531e725cSNickeau            $jsonString = substr($match, 7, -3);
122007225e5Sgerardnico
123007225e5Sgerardnico            // Empty front matter
124531e725cSNickeau            if (trim($jsonString) == "") {
125a6bf47aaSNickeau                $this->deleteKnownMetaThatAreNoMorePresent();
1265f891b7eSNickeau                return array(self::STATUS => self::PARSING_STATE_EMPTY);
127007225e5Sgerardnico            }
128007225e5Sgerardnico
129007225e5Sgerardnico            // Otherwise you get an object ie $arrayFormat-> syntax
130007225e5Sgerardnico            $arrayFormat = true;
131531e725cSNickeau            $jsonArray = json_decode($jsonString, $arrayFormat);
132007225e5Sgerardnico
133a6bf47aaSNickeau            $result = [];
134007225e5Sgerardnico            // Decodage problem
135531e725cSNickeau            if ($jsonArray == null) {
136a6bf47aaSNickeau                $result[self::STATUS] = self::PARSING_STATE_ERROR;
137a6bf47aaSNickeau                $result[PluginUtility::PAYLOAD] = $match;
138a6bf47aaSNickeau            } else {
1395f891b7eSNickeau                $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
140a6bf47aaSNickeau                $result[PluginUtility::ATTRIBUTES] = $jsonArray;
141a6bf47aaSNickeau            }
142531e725cSNickeau
143531e725cSNickeau            /**
144531e725cSNickeau             * End position is the length of the match + 1 for the newline
145531e725cSNickeau             */
146531e725cSNickeau            $newLine = 1;
147531e725cSNickeau            $endPosition = $pos + strlen($match) + $newLine;
148531e725cSNickeau            $result[PluginUtility::POSITION] = [$pos, $endPosition];
149007225e5Sgerardnico
150007225e5Sgerardnico            return $result;
151007225e5Sgerardnico        }
152007225e5Sgerardnico
153007225e5Sgerardnico        return array();
154007225e5Sgerardnico    }
155007225e5Sgerardnico
156007225e5Sgerardnico    /**
157007225e5Sgerardnico     * Render the output
158007225e5Sgerardnico     * @param string $format
159007225e5Sgerardnico     * @param Doku_Renderer $renderer
160007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
161007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
162007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
163007225e5Sgerardnico     *
164007225e5Sgerardnico     *
165007225e5Sgerardnico     */
166007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
167007225e5Sgerardnico    {
168007225e5Sgerardnico
169007225e5Sgerardnico        switch ($format) {
170007225e5Sgerardnico            case 'xhtml':
171007225e5Sgerardnico                global $ID;
172007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
17321913ab3SNickeau
1745f891b7eSNickeau                $state = $data[self::STATUS];
175007225e5Sgerardnico                if ($state == self::PARSING_STATE_ERROR) {
1765f891b7eSNickeau                    $json = $data[PluginUtility::PAYLOAD];
1775f891b7eSNickeau                    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);
178007225e5Sgerardnico                }
17921913ab3SNickeau
18021913ab3SNickeau                /**
18121913ab3SNickeau                 * Section
18221913ab3SNickeau                 */
18321913ab3SNickeau                list($startPosition, $endPosition) = $data[PluginUtility::POSITION];
18421913ab3SNickeau                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
18521913ab3SNickeau                    $position = $startPosition;
18621913ab3SNickeau                    $name = self::CANONICAL;
18721913ab3SNickeau                    PluginUtility::startSection($renderer, $position, $name);
18821913ab3SNickeau                    $renderer->finishSectionEdit($endPosition);
18921913ab3SNickeau                }
190007225e5Sgerardnico                break;
191531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
192a6bf47aaSNickeau
193a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
194a6bf47aaSNickeau                    return false;
195a6bf47aaSNickeau                }
196a6bf47aaSNickeau
197007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
198a6bf47aaSNickeau                $jsonArray = $data[PluginUtility::ATTRIBUTES];
199a6bf47aaSNickeau                if (array_key_exists("description", $jsonArray)) {
200a6bf47aaSNickeau                    $renderer->setMeta("description", $jsonArray["description"]);
201007225e5Sgerardnico                }
202a6bf47aaSNickeau                if (array_key_exists(Page::CANONICAL_PROPERTY, $jsonArray)) {
203a6bf47aaSNickeau                    $renderer->setMeta(Page::CANONICAL_PROPERTY, $jsonArray[Page::CANONICAL_PROPERTY]);
204007225e5Sgerardnico                }
205a6bf47aaSNickeau                if (array_key_exists(Page::TITLE_PROPERTY, $jsonArray)) {
206a6bf47aaSNickeau                    $renderer->setMeta(Page::TITLE_PROPERTY, $jsonArray[Page::TITLE_PROPERTY]);
207f3748b38Sgerardnico                }
208a6bf47aaSNickeau                if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $jsonArray)) {
209a6bf47aaSNickeau                    $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $jsonArray[Page::LOW_QUALITY_PAGE_INDICATOR]);
2109b9e6d1fSgerardnico                }
211007225e5Sgerardnico                break;
212a6bf47aaSNickeau            case "metadata":
213a6bf47aaSNickeau
214a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
215a6bf47aaSNickeau                    return false;
216a6bf47aaSNickeau                }
217a6bf47aaSNickeau
218a6bf47aaSNickeau                global $ID;
219a6bf47aaSNickeau                $jsonArray = $data[PluginUtility::ATTRIBUTES];
220a6bf47aaSNickeau
221a6bf47aaSNickeau
222a6bf47aaSNickeau                $notModifiableMeta = [
223a6bf47aaSNickeau                    "date",
224a6bf47aaSNickeau                    "user",
225a6bf47aaSNickeau                    "last_change",
226a6bf47aaSNickeau                    "creator",
227a6bf47aaSNickeau                    "contributor"
228a6bf47aaSNickeau                ];
229a6bf47aaSNickeau
230a6bf47aaSNickeau                foreach ($jsonArray as $key => $value) {
231a6bf47aaSNickeau
232a6bf47aaSNickeau                    $lowerCaseKey = trim(strtolower($key));
233a6bf47aaSNickeau
234a6bf47aaSNickeau                    // Not modifiable metadata
235a6bf47aaSNickeau                    if (in_array($lowerCaseKey, $notModifiableMeta)) {
236a6bf47aaSNickeau                        LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
237a6bf47aaSNickeau                        continue;
238a6bf47aaSNickeau                    }
239a6bf47aaSNickeau
240a6bf47aaSNickeau                    switch ($lowerCaseKey) {
241a6bf47aaSNickeau
242a6bf47aaSNickeau                        case Page::DESCRIPTION_PROPERTY:
243a6bf47aaSNickeau                            /**
244a6bf47aaSNickeau                             * Overwrite also the actual description
245a6bf47aaSNickeau                             */
246a6bf47aaSNickeau                            p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
247a6bf47aaSNickeau                                "abstract" => $value,
248a6bf47aaSNickeau                                "origin" => syntax_plugin_combo_frontmatter::CANONICAL
249a6bf47aaSNickeau                            )));
250a6bf47aaSNickeau                            /**
251a6bf47aaSNickeau                             * Continue because
252a6bf47aaSNickeau                             * the description value was already stored
253a6bf47aaSNickeau                             * We don't want to override it
254a6bf47aaSNickeau                             * And continue 2 because continue == break in a switch
255a6bf47aaSNickeau                             */
256a6bf47aaSNickeau                            continue 2;
257a6bf47aaSNickeau
258a6bf47aaSNickeau
259a6bf47aaSNickeau                        // Canonical should be lowercase
260a6bf47aaSNickeau                        case Page::CANONICAL_PROPERTY:
261a6bf47aaSNickeau                            $value = strtolower($value);
262a6bf47aaSNickeau                            break;
263a6bf47aaSNickeau
264a6bf47aaSNickeau                    }
265a6bf47aaSNickeau                    // Set the value persistently
266a6bf47aaSNickeau                    p_set_metadata($ID, array($lowerCaseKey => $value));
267a6bf47aaSNickeau
268a6bf47aaSNickeau                }
269a6bf47aaSNickeau
270a6bf47aaSNickeau                $this->deleteKnownMetaThatAreNoMorePresent($jsonArray);
271a6bf47aaSNickeau
272a6bf47aaSNickeau                break;
273007225e5Sgerardnico
274007225e5Sgerardnico        }
275007225e5Sgerardnico        return true;
276007225e5Sgerardnico    }
277007225e5Sgerardnico
278007225e5Sgerardnico    /**
279007225e5Sgerardnico     *
280007225e5Sgerardnico     * @param array $json - The Json
281007225e5Sgerardnico     * Delete the controlled meta that are no more present if they exists
282007225e5Sgerardnico     * @return bool
283007225e5Sgerardnico     */
2845f891b7eSNickeau    public
285a6bf47aaSNickeau    function deleteKnownMetaThatAreNoMorePresent(array $json = array())
286007225e5Sgerardnico    {
287007225e5Sgerardnico        global $ID;
288007225e5Sgerardnico
289007225e5Sgerardnico        /**
290007225e5Sgerardnico         * The managed meta with the exception of
291007225e5Sgerardnico         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
292007225e5Sgerardnico         * because it's already managed by dokuwiki in description['abstract']
293007225e5Sgerardnico         */
294007225e5Sgerardnico        $managedMeta = [
29571f916b9Sgerardnico            Page::CANONICAL_PROPERTY,
296007225e5Sgerardnico            action_plugin_combo_metatitle::TITLE_META_KEY,
297007225e5Sgerardnico            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER
298007225e5Sgerardnico        ];
299007225e5Sgerardnico        $meta = p_read_metadata($ID);
300007225e5Sgerardnico        foreach ($managedMeta as $metaKey) {
301007225e5Sgerardnico            if (!array_key_exists($metaKey, $json)) {
302007225e5Sgerardnico                if (isset($meta['persistent'][$metaKey])) {
303007225e5Sgerardnico                    unset($meta['persistent'][$metaKey]);
304007225e5Sgerardnico                }
305007225e5Sgerardnico            }
306007225e5Sgerardnico        }
307007225e5Sgerardnico        return p_save_metadata($ID, $meta);
308007225e5Sgerardnico    }
309007225e5Sgerardnico
310007225e5Sgerardnico
311007225e5Sgerardnico}
312007225e5Sgerardnico
313