xref: /plugin/combo/syntax/frontmatter.php (revision 531e725cdb5a652164f2d97f556304e31f720033)
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