xref: /plugin/combo/syntax/frontmatter.php (revision 21913ab3235d516e2fa19c7e3929b555b3a2bda1)
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";
47*21913ab3SNickeau    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
64007225e5Sgerardnico    /**
65007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
66007225e5Sgerardnico     * Higher number than the teaser-columns
67007225e5Sgerardnico     * because the mode with the lowest sort number will win out
68007225e5Sgerardnico     */
69007225e5Sgerardnico    function getSort()
70007225e5Sgerardnico    {
71007225e5Sgerardnico        return 99;
72007225e5Sgerardnico    }
73007225e5Sgerardnico
74007225e5Sgerardnico    /**
75007225e5Sgerardnico     * Create a pattern that will called this plugin
76007225e5Sgerardnico     *
77007225e5Sgerardnico     * @param string $mode
78007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
79007225e5Sgerardnico     */
80007225e5Sgerardnico    function connectTo($mode)
81007225e5Sgerardnico    {
82007225e5Sgerardnico        if ($mode == "base") {
83007225e5Sgerardnico            // only from the top
84007225e5Sgerardnico            $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent()));
85007225e5Sgerardnico        }
86007225e5Sgerardnico    }
87007225e5Sgerardnico
88007225e5Sgerardnico    /**
89007225e5Sgerardnico     *
90007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
91007225e5Sgerardnico     * and to return the result for use in the renderer
92007225e5Sgerardnico     * This result is always cached until the page is modified.
93007225e5Sgerardnico     * @param string $match
94007225e5Sgerardnico     * @param int $state
95007225e5Sgerardnico     * @param int $pos
96007225e5Sgerardnico     * @param Doku_Handler $handler
97007225e5Sgerardnico     * @return array|bool
98007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
99007225e5Sgerardnico     *
100007225e5Sgerardnico     */
101007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
102007225e5Sgerardnico    {
103007225e5Sgerardnico
104007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
105007225e5Sgerardnico
106007225e5Sgerardnico            global $ID;
107007225e5Sgerardnico
108007225e5Sgerardnico            // strip
109007225e5Sgerardnico            //   from start `---json` + eol = 8
110007225e5Sgerardnico            //   from end   `---` + eol = 4
111007225e5Sgerardnico            $match = substr($match, 7, -3);
112007225e5Sgerardnico
113007225e5Sgerardnico            // Empty front matter
114007225e5Sgerardnico            if (trim($match) == "") {
115007225e5Sgerardnico                $this->closeParsing();
1165f891b7eSNickeau                return array(self::STATUS => self::PARSING_STATE_EMPTY);
117007225e5Sgerardnico            }
118007225e5Sgerardnico
119007225e5Sgerardnico            // Otherwise you get an object ie $arrayFormat-> syntax
120007225e5Sgerardnico            $arrayFormat = true;
121007225e5Sgerardnico            $json = json_decode($match, $arrayFormat);
122007225e5Sgerardnico
123007225e5Sgerardnico            // Decodage problem
124007225e5Sgerardnico            if ($json == null) {
1255f891b7eSNickeau                return array(
1265f891b7eSNickeau                    self::STATUS => self::PARSING_STATE_ERROR,
1275f891b7eSNickeau                    PluginUtility::PAYLOAD => $match
1285f891b7eSNickeau                );
129007225e5Sgerardnico            }
130007225e5Sgerardnico
131007225e5Sgerardnico            $notModifiableMeta = [
132007225e5Sgerardnico                "date",
133007225e5Sgerardnico                "user",
134007225e5Sgerardnico                "last_change",
135007225e5Sgerardnico                "creator",
136007225e5Sgerardnico                "contributor"
137007225e5Sgerardnico            ];
138007225e5Sgerardnico            $result = array();
139007225e5Sgerardnico            foreach ($json as $key => $value) {
140007225e5Sgerardnico
1415f891b7eSNickeau                $lowerCaseKey = trim(strtolower($key));
1425f891b7eSNickeau
143007225e5Sgerardnico                // Not modifiable metadata
1445f891b7eSNickeau                if (in_array($lowerCaseKey, $notModifiableMeta)) {
1455f891b7eSNickeau                    LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
146007225e5Sgerardnico                    continue;
147007225e5Sgerardnico                }
148007225e5Sgerardnico
1495f891b7eSNickeau                switch ($lowerCaseKey) {
1505f891b7eSNickeau
1515f891b7eSNickeau                    case Page::DESCRIPTION_PROPERTY:
152007225e5Sgerardnico                        $result["description"] = $value;
1535f891b7eSNickeau                        /**
1545f891b7eSNickeau                         * Overwrite also the actual description
1555f891b7eSNickeau                         */
1565f891b7eSNickeau                        p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
1575f891b7eSNickeau                            "abstract" => $value,
1585f891b7eSNickeau                            "origin" => syntax_plugin_combo_frontmatter::CANONICAL
1595f891b7eSNickeau                        )));
1605f891b7eSNickeau                        /**
1615f891b7eSNickeau                         * Continue because
1625f891b7eSNickeau                         * the description value was already stored
1635f891b7eSNickeau                         * We don't want to override it
1645f891b7eSNickeau                         * And continue 2 because continue == break in a switch
1655f891b7eSNickeau                         */
1665f891b7eSNickeau                        continue 2;
1675f891b7eSNickeau                        break;
168007225e5Sgerardnico
169f3748b38Sgerardnico                    /**
1709b9e6d1fSgerardnico                     * Pass the title to the metadata
171f3748b38Sgerardnico                     * to advertise that it's in the front-matter
172f3748b38Sgerardnico                     * for the quality rules
173f3748b38Sgerardnico                     */
1745f891b7eSNickeau                    case Page::TITLE_PROPERTY:
175f3748b38Sgerardnico                        $result[Page::TITLE_PROPERTY] = $value;
1765f891b7eSNickeau                        break;
177f3748b38Sgerardnico
1789b9e6d1fSgerardnico                    /**
1799b9e6d1fSgerardnico                     * Pass the low quality indicator
1809b9e6d1fSgerardnico                     * to advertise that it's in the front-matter
1819b9e6d1fSgerardnico                     */
1825f891b7eSNickeau                    case Page::LOW_QUALITY_PAGE_INDICATOR:
1839b9e6d1fSgerardnico                        $result[Page::LOW_QUALITY_PAGE_INDICATOR] = $value;
1845f891b7eSNickeau                        break;
1859b9e6d1fSgerardnico
186007225e5Sgerardnico                    // Canonical should be lowercase
1875f891b7eSNickeau                    case Page::CANONICAL_PROPERTY:
18871f916b9Sgerardnico                        $result[Page::CANONICAL_PROPERTY] = $value;
189007225e5Sgerardnico                        $value = strtolower($value);
1905f891b7eSNickeau                        break;
191007225e5Sgerardnico
1925f891b7eSNickeau                }
193007225e5Sgerardnico                // Set the value persistently
1945f891b7eSNickeau                p_set_metadata($ID, array($lowerCaseKey => $value));
195007225e5Sgerardnico
196007225e5Sgerardnico            }
197007225e5Sgerardnico
198007225e5Sgerardnico            $this->closeParsing($json);
199007225e5Sgerardnico
2005f891b7eSNickeau            $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
201*21913ab3SNickeau            $result[PluginUtility::POSITION]=[$pos,$pos + strlen($match) + 1];
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 */
226*21913ab3SNickeau
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                }
232*21913ab3SNickeau
233*21913ab3SNickeau                /**
234*21913ab3SNickeau                 * Section
235*21913ab3SNickeau                 */
236*21913ab3SNickeau                list($startPosition,$endPosition) =  $data[PluginUtility::POSITION];
237*21913ab3SNickeau                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING,1)) {
238*21913ab3SNickeau                    $position = $startPosition;
239*21913ab3SNickeau                    $name = self::CANONICAL;
240*21913ab3SNickeau                    PluginUtility::startSection($renderer, $position, $name);
241*21913ab3SNickeau                    $renderer->finishSectionEdit($endPosition);
242*21913ab3SNickeau                }
243007225e5Sgerardnico                break;
244007225e5Sgerardnico            case Analytics::RENDERER_FORMAT:
245007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
246007225e5Sgerardnico                if (array_key_exists("description", $data)) {
247007225e5Sgerardnico                    $renderer->setMeta("description", $data["description"]);
248007225e5Sgerardnico                }
24971f916b9Sgerardnico                if (array_key_exists(Page::CANONICAL_PROPERTY, $data)) {
25071f916b9Sgerardnico                    $renderer->setMeta(Page::CANONICAL_PROPERTY, $data[Page::CANONICAL_PROPERTY]);
251007225e5Sgerardnico                }
252f3748b38Sgerardnico                if (array_key_exists(Page::TITLE_PROPERTY, $data)) {
253f3748b38Sgerardnico                    $renderer->setMeta(Page::TITLE_PROPERTY, $data[Page::TITLE_PROPERTY]);
254f3748b38Sgerardnico                }
2559b9e6d1fSgerardnico                if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $data)) {
2569b9e6d1fSgerardnico                    $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $data[Page::LOW_QUALITY_PAGE_INDICATOR]);
2579b9e6d1fSgerardnico                }
258007225e5Sgerardnico                break;
259007225e5Sgerardnico
260007225e5Sgerardnico        }
261007225e5Sgerardnico        return true;
262007225e5Sgerardnico    }
263007225e5Sgerardnico
264007225e5Sgerardnico    /**
265007225e5Sgerardnico     *
266007225e5Sgerardnico     * @param array $json - The Json
267007225e5Sgerardnico     * Delete the controlled meta that are no more present if they exists
268007225e5Sgerardnico     * @return bool
269007225e5Sgerardnico     */
2705f891b7eSNickeau    public
2715f891b7eSNickeau    function closeParsing(array $json = array())
272007225e5Sgerardnico    {
273007225e5Sgerardnico        global $ID;
274007225e5Sgerardnico
275007225e5Sgerardnico        /**
276007225e5Sgerardnico         * The managed meta with the exception of
277007225e5Sgerardnico         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
278007225e5Sgerardnico         * because it's already managed by dokuwiki in description['abstract']
279007225e5Sgerardnico         */
280007225e5Sgerardnico        $managedMeta = [
28171f916b9Sgerardnico            Page::CANONICAL_PROPERTY,
282007225e5Sgerardnico            action_plugin_combo_metatitle::TITLE_META_KEY,
283007225e5Sgerardnico            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER
284007225e5Sgerardnico        ];
285007225e5Sgerardnico        $meta = p_read_metadata($ID);
286007225e5Sgerardnico        foreach ($managedMeta as $metaKey) {
287007225e5Sgerardnico            if (!array_key_exists($metaKey, $json)) {
288007225e5Sgerardnico                if (isset($meta['persistent'][$metaKey])) {
289007225e5Sgerardnico                    unset($meta['persistent'][$metaKey]);
290007225e5Sgerardnico                }
291007225e5Sgerardnico            }
292007225e5Sgerardnico        }
293007225e5Sgerardnico        return p_save_metadata($ID, $meta);
294007225e5Sgerardnico    }
295007225e5Sgerardnico
296007225e5Sgerardnico
297007225e5Sgerardnico}
298007225e5Sgerardnico
299