xref: /plugin/combo/syntax/frontmatter.php (revision 9337a630db122fdba0294f47d72bdf5433c2bf10)
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    {
65531e725cSNickeau        return "normal";
66531e725cSNickeau    }
67531e725cSNickeau
68531e725cSNickeau
69007225e5Sgerardnico    /**
70007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
71007225e5Sgerardnico     * Higher number than the teaser-columns
72007225e5Sgerardnico     * because the mode with the lowest sort number will win out
73007225e5Sgerardnico     */
74007225e5Sgerardnico    function getSort()
75007225e5Sgerardnico    {
76007225e5Sgerardnico        return 99;
77007225e5Sgerardnico    }
78007225e5Sgerardnico
79007225e5Sgerardnico    /**
80007225e5Sgerardnico     * Create a pattern that will called this plugin
81007225e5Sgerardnico     *
82007225e5Sgerardnico     * @param string $mode
83007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
84007225e5Sgerardnico     */
85007225e5Sgerardnico    function connectTo($mode)
86007225e5Sgerardnico    {
87007225e5Sgerardnico        if ($mode == "base") {
88007225e5Sgerardnico            // only from the top
89*9337a630SNickeau            $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
90007225e5Sgerardnico        }
91007225e5Sgerardnico    }
92007225e5Sgerardnico
93007225e5Sgerardnico    /**
94007225e5Sgerardnico     *
95007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
96007225e5Sgerardnico     * and to return the result for use in the renderer
97007225e5Sgerardnico     * This result is always cached until the page is modified.
98007225e5Sgerardnico     * @param string $match
99007225e5Sgerardnico     * @param int $state
100007225e5Sgerardnico     * @param int $pos
101007225e5Sgerardnico     * @param Doku_Handler $handler
102007225e5Sgerardnico     * @return array|bool
103007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
104007225e5Sgerardnico     *
105007225e5Sgerardnico     */
106007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
107007225e5Sgerardnico    {
108007225e5Sgerardnico
109007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
110007225e5Sgerardnico
111007225e5Sgerardnico            // strip
112007225e5Sgerardnico            //   from start `---json` + eol = 8
113007225e5Sgerardnico            //   from end   `---` + eol = 4
114531e725cSNickeau            $jsonString = substr($match, 7, -3);
115007225e5Sgerardnico
116007225e5Sgerardnico            // Empty front matter
117531e725cSNickeau            if (trim($jsonString) == "") {
118a6bf47aaSNickeau                $this->deleteKnownMetaThatAreNoMorePresent();
1195f891b7eSNickeau                return array(self::STATUS => self::PARSING_STATE_EMPTY);
120007225e5Sgerardnico            }
121007225e5Sgerardnico
122007225e5Sgerardnico            // Otherwise you get an object ie $arrayFormat-> syntax
123007225e5Sgerardnico            $arrayFormat = true;
124531e725cSNickeau            $jsonArray = json_decode($jsonString, $arrayFormat);
125007225e5Sgerardnico
126a6bf47aaSNickeau            $result = [];
127007225e5Sgerardnico            // Decodage problem
128531e725cSNickeau            if ($jsonArray == null) {
129a6bf47aaSNickeau                $result[self::STATUS] = self::PARSING_STATE_ERROR;
130a6bf47aaSNickeau                $result[PluginUtility::PAYLOAD] = $match;
131a6bf47aaSNickeau            } else {
1325f891b7eSNickeau                $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
133a6bf47aaSNickeau                $result[PluginUtility::ATTRIBUTES] = $jsonArray;
134a6bf47aaSNickeau            }
135531e725cSNickeau
136531e725cSNickeau            /**
137531e725cSNickeau             * End position is the length of the match + 1 for the newline
138531e725cSNickeau             */
139531e725cSNickeau            $newLine = 1;
140531e725cSNickeau            $endPosition = $pos + strlen($match) + $newLine;
141531e725cSNickeau            $result[PluginUtility::POSITION] = [$pos, $endPosition];
142007225e5Sgerardnico
143007225e5Sgerardnico            return $result;
144007225e5Sgerardnico        }
145007225e5Sgerardnico
146007225e5Sgerardnico        return array();
147007225e5Sgerardnico    }
148007225e5Sgerardnico
149007225e5Sgerardnico    /**
150007225e5Sgerardnico     * Render the output
151007225e5Sgerardnico     * @param string $format
152007225e5Sgerardnico     * @param Doku_Renderer $renderer
153007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
154007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
155007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
156007225e5Sgerardnico     *
157007225e5Sgerardnico     *
158007225e5Sgerardnico     */
159007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
160007225e5Sgerardnico    {
161007225e5Sgerardnico
162007225e5Sgerardnico        switch ($format) {
163007225e5Sgerardnico            case 'xhtml':
164007225e5Sgerardnico                global $ID;
165007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
16621913ab3SNickeau
1675f891b7eSNickeau                $state = $data[self::STATUS];
168007225e5Sgerardnico                if ($state == self::PARSING_STATE_ERROR) {
1695f891b7eSNickeau                    $json = $data[PluginUtility::PAYLOAD];
1705f891b7eSNickeau                    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);
171007225e5Sgerardnico                }
17221913ab3SNickeau
17321913ab3SNickeau                /**
17421913ab3SNickeau                 * Section
17521913ab3SNickeau                 */
17621913ab3SNickeau                list($startPosition, $endPosition) = $data[PluginUtility::POSITION];
17721913ab3SNickeau                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
17821913ab3SNickeau                    $position = $startPosition;
17921913ab3SNickeau                    $name = self::CANONICAL;
18021913ab3SNickeau                    PluginUtility::startSection($renderer, $position, $name);
18121913ab3SNickeau                    $renderer->finishSectionEdit($endPosition);
18221913ab3SNickeau                }
183007225e5Sgerardnico                break;
184531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
185a6bf47aaSNickeau
186a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
187a6bf47aaSNickeau                    return false;
188a6bf47aaSNickeau                }
189a6bf47aaSNickeau
190007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
191a6bf47aaSNickeau                $jsonArray = $data[PluginUtility::ATTRIBUTES];
192a6bf47aaSNickeau                if (array_key_exists("description", $jsonArray)) {
193a6bf47aaSNickeau                    $renderer->setMeta("description", $jsonArray["description"]);
194007225e5Sgerardnico                }
195a6bf47aaSNickeau                if (array_key_exists(Page::CANONICAL_PROPERTY, $jsonArray)) {
196a6bf47aaSNickeau                    $renderer->setMeta(Page::CANONICAL_PROPERTY, $jsonArray[Page::CANONICAL_PROPERTY]);
197007225e5Sgerardnico                }
198a6bf47aaSNickeau                if (array_key_exists(Page::TITLE_PROPERTY, $jsonArray)) {
199a6bf47aaSNickeau                    $renderer->setMeta(Page::TITLE_PROPERTY, $jsonArray[Page::TITLE_PROPERTY]);
200f3748b38Sgerardnico                }
201a6bf47aaSNickeau                if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $jsonArray)) {
202a6bf47aaSNickeau                    $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $jsonArray[Page::LOW_QUALITY_PAGE_INDICATOR]);
2039b9e6d1fSgerardnico                }
204007225e5Sgerardnico                break;
205a6bf47aaSNickeau            case "metadata":
206a6bf47aaSNickeau
207a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
208a6bf47aaSNickeau                    return false;
209a6bf47aaSNickeau                }
210a6bf47aaSNickeau
211a6bf47aaSNickeau                global $ID;
212a6bf47aaSNickeau                $jsonArray = $data[PluginUtility::ATTRIBUTES];
213a6bf47aaSNickeau
214a6bf47aaSNickeau
215a6bf47aaSNickeau                $notModifiableMeta = [
216a6bf47aaSNickeau                    "date",
217a6bf47aaSNickeau                    "user",
218a6bf47aaSNickeau                    "last_change",
219a6bf47aaSNickeau                    "creator",
220a6bf47aaSNickeau                    "contributor"
221a6bf47aaSNickeau                ];
222a6bf47aaSNickeau
223a6bf47aaSNickeau                foreach ($jsonArray as $key => $value) {
224a6bf47aaSNickeau
225a6bf47aaSNickeau                    $lowerCaseKey = trim(strtolower($key));
226a6bf47aaSNickeau
227a6bf47aaSNickeau                    // Not modifiable metadata
228a6bf47aaSNickeau                    if (in_array($lowerCaseKey, $notModifiableMeta)) {
229a6bf47aaSNickeau                        LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
230a6bf47aaSNickeau                        continue;
231a6bf47aaSNickeau                    }
232a6bf47aaSNickeau
233a6bf47aaSNickeau                    switch ($lowerCaseKey) {
234a6bf47aaSNickeau
235a6bf47aaSNickeau                        case Page::DESCRIPTION_PROPERTY:
236a6bf47aaSNickeau                            /**
237a6bf47aaSNickeau                             * Overwrite also the actual description
238a6bf47aaSNickeau                             */
239a6bf47aaSNickeau                            p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
240a6bf47aaSNickeau                                "abstract" => $value,
241a6bf47aaSNickeau                                "origin" => syntax_plugin_combo_frontmatter::CANONICAL
242a6bf47aaSNickeau                            )));
243a6bf47aaSNickeau                            /**
244a6bf47aaSNickeau                             * Continue because
245a6bf47aaSNickeau                             * the description value was already stored
246a6bf47aaSNickeau                             * We don't want to override it
247a6bf47aaSNickeau                             * And continue 2 because continue == break in a switch
248a6bf47aaSNickeau                             */
249a6bf47aaSNickeau                            continue 2;
250a6bf47aaSNickeau
251a6bf47aaSNickeau
252a6bf47aaSNickeau                        // Canonical should be lowercase
253a6bf47aaSNickeau                        case Page::CANONICAL_PROPERTY:
254a6bf47aaSNickeau                            $value = strtolower($value);
255a6bf47aaSNickeau                            break;
256a6bf47aaSNickeau
257a6bf47aaSNickeau                    }
258a6bf47aaSNickeau                    // Set the value persistently
259a6bf47aaSNickeau                    p_set_metadata($ID, array($lowerCaseKey => $value));
260a6bf47aaSNickeau
261a6bf47aaSNickeau                }
262a6bf47aaSNickeau
263a6bf47aaSNickeau                $this->deleteKnownMetaThatAreNoMorePresent($jsonArray);
264a6bf47aaSNickeau
265a6bf47aaSNickeau                break;
266007225e5Sgerardnico
267007225e5Sgerardnico        }
268007225e5Sgerardnico        return true;
269007225e5Sgerardnico    }
270007225e5Sgerardnico
271007225e5Sgerardnico    /**
272007225e5Sgerardnico     *
273007225e5Sgerardnico     * @param array $json - The Json
274007225e5Sgerardnico     * Delete the controlled meta that are no more present if they exists
275007225e5Sgerardnico     * @return bool
276007225e5Sgerardnico     */
2775f891b7eSNickeau    public
278a6bf47aaSNickeau    function deleteKnownMetaThatAreNoMorePresent(array $json = array())
279007225e5Sgerardnico    {
280007225e5Sgerardnico        global $ID;
281007225e5Sgerardnico
282007225e5Sgerardnico        /**
283007225e5Sgerardnico         * The managed meta with the exception of
284007225e5Sgerardnico         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
285007225e5Sgerardnico         * because it's already managed by dokuwiki in description['abstract']
286007225e5Sgerardnico         */
287007225e5Sgerardnico        $managedMeta = [
28871f916b9Sgerardnico            Page::CANONICAL_PROPERTY,
289007225e5Sgerardnico            action_plugin_combo_metatitle::TITLE_META_KEY,
290007225e5Sgerardnico            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER
291007225e5Sgerardnico        ];
292007225e5Sgerardnico        $meta = p_read_metadata($ID);
293007225e5Sgerardnico        foreach ($managedMeta as $metaKey) {
294007225e5Sgerardnico            if (!array_key_exists($metaKey, $json)) {
295007225e5Sgerardnico                if (isset($meta['persistent'][$metaKey])) {
296007225e5Sgerardnico                    unset($meta['persistent'][$metaKey]);
297007225e5Sgerardnico                }
298007225e5Sgerardnico            }
299007225e5Sgerardnico        }
300007225e5Sgerardnico        return p_save_metadata($ID, $meta);
301007225e5Sgerardnico    }
302007225e5Sgerardnico
303007225e5Sgerardnico
304007225e5Sgerardnico}
305007225e5Sgerardnico
306