xref: /plugin/combo/syntax/frontmatter.php (revision 5f891b7e09648e05e78f5882f3fdde1e9df9b0f1)
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";
45*5f891b7eSNickeau    const STATUS = "status";
46*5f891b7eSNickeau    const CANONICAL = "frontmatter";
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
63007225e5Sgerardnico    /**
64007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
65007225e5Sgerardnico     * Higher number than the teaser-columns
66007225e5Sgerardnico     * because the mode with the lowest sort number will win out
67007225e5Sgerardnico     */
68007225e5Sgerardnico    function getSort()
69007225e5Sgerardnico    {
70007225e5Sgerardnico        return 99;
71007225e5Sgerardnico    }
72007225e5Sgerardnico
73007225e5Sgerardnico    /**
74007225e5Sgerardnico     * Create a pattern that will called this plugin
75007225e5Sgerardnico     *
76007225e5Sgerardnico     * @param string $mode
77007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
78007225e5Sgerardnico     */
79007225e5Sgerardnico    function connectTo($mode)
80007225e5Sgerardnico    {
81007225e5Sgerardnico        if ($mode == "base") {
82007225e5Sgerardnico            // only from the top
83007225e5Sgerardnico            $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent()));
84007225e5Sgerardnico        }
85007225e5Sgerardnico    }
86007225e5Sgerardnico
87007225e5Sgerardnico    /**
88007225e5Sgerardnico     *
89007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
90007225e5Sgerardnico     * and to return the result for use in the renderer
91007225e5Sgerardnico     * This result is always cached until the page is modified.
92007225e5Sgerardnico     * @param string $match
93007225e5Sgerardnico     * @param int $state
94007225e5Sgerardnico     * @param int $pos
95007225e5Sgerardnico     * @param Doku_Handler $handler
96007225e5Sgerardnico     * @return array|bool
97007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
98007225e5Sgerardnico     *
99007225e5Sgerardnico     */
100007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
101007225e5Sgerardnico    {
102007225e5Sgerardnico
103007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
104007225e5Sgerardnico
105007225e5Sgerardnico            global $ID;
106007225e5Sgerardnico
107007225e5Sgerardnico            // strip
108007225e5Sgerardnico            //   from start `---json` + eol = 8
109007225e5Sgerardnico            //   from end   `---` + eol = 4
110007225e5Sgerardnico            $match = substr($match, 7, -3);
111007225e5Sgerardnico
112007225e5Sgerardnico            // Empty front matter
113007225e5Sgerardnico            if (trim($match) == "") {
114007225e5Sgerardnico                $this->closeParsing();
115*5f891b7eSNickeau                return array(self::STATUS => self::PARSING_STATE_EMPTY);
116007225e5Sgerardnico            }
117007225e5Sgerardnico
118007225e5Sgerardnico            // Otherwise you get an object ie $arrayFormat-> syntax
119007225e5Sgerardnico            $arrayFormat = true;
120007225e5Sgerardnico            $json = json_decode($match, $arrayFormat);
121007225e5Sgerardnico
122007225e5Sgerardnico            // Decodage problem
123007225e5Sgerardnico            if ($json == null) {
124*5f891b7eSNickeau                return array(
125*5f891b7eSNickeau                    self::STATUS => self::PARSING_STATE_ERROR,
126*5f891b7eSNickeau                    PluginUtility::PAYLOAD => $match
127*5f891b7eSNickeau                );
128007225e5Sgerardnico            }
129007225e5Sgerardnico
130007225e5Sgerardnico            $notModifiableMeta = [
131007225e5Sgerardnico                "date",
132007225e5Sgerardnico                "user",
133007225e5Sgerardnico                "last_change",
134007225e5Sgerardnico                "creator",
135007225e5Sgerardnico                "contributor"
136007225e5Sgerardnico            ];
137007225e5Sgerardnico            $result = array();
138007225e5Sgerardnico            foreach ($json as $key => $value) {
139007225e5Sgerardnico
140*5f891b7eSNickeau                $lowerCaseKey = trim(strtolower($key));
141*5f891b7eSNickeau
142007225e5Sgerardnico                // Not modifiable metadata
143*5f891b7eSNickeau                if (in_array($lowerCaseKey, $notModifiableMeta)) {
144*5f891b7eSNickeau                    LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
145007225e5Sgerardnico                    continue;
146007225e5Sgerardnico                }
147007225e5Sgerardnico
148*5f891b7eSNickeau                switch ($lowerCaseKey) {
149*5f891b7eSNickeau
150*5f891b7eSNickeau                    case Page::DESCRIPTION_PROPERTY:
151007225e5Sgerardnico                        $result["description"] = $value;
152*5f891b7eSNickeau                        /**
153*5f891b7eSNickeau                         * Overwrite also the actual description
154*5f891b7eSNickeau                         */
155*5f891b7eSNickeau                        p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
156*5f891b7eSNickeau                            "abstract" => $value,
157*5f891b7eSNickeau                            "origin" => syntax_plugin_combo_frontmatter::CANONICAL
158*5f891b7eSNickeau                        )));
159*5f891b7eSNickeau                        /**
160*5f891b7eSNickeau                         * Continue because
161*5f891b7eSNickeau                         * the description value was already stored
162*5f891b7eSNickeau                         * We don't want to override it
163*5f891b7eSNickeau                         * And continue 2 because continue == break in a switch
164*5f891b7eSNickeau                         */
165*5f891b7eSNickeau                        continue 2;
166*5f891b7eSNickeau                        break;
167007225e5Sgerardnico
168f3748b38Sgerardnico                    /**
1699b9e6d1fSgerardnico                     * Pass the title to the metadata
170f3748b38Sgerardnico                     * to advertise that it's in the front-matter
171f3748b38Sgerardnico                     * for the quality rules
172f3748b38Sgerardnico                     */
173*5f891b7eSNickeau                    case Page::TITLE_PROPERTY:
174f3748b38Sgerardnico                        $result[Page::TITLE_PROPERTY] = $value;
175*5f891b7eSNickeau                        break;
176f3748b38Sgerardnico
1779b9e6d1fSgerardnico                    /**
1789b9e6d1fSgerardnico                     * Pass the low quality indicator
1799b9e6d1fSgerardnico                     * to advertise that it's in the front-matter
1809b9e6d1fSgerardnico                     */
181*5f891b7eSNickeau                    case Page::LOW_QUALITY_PAGE_INDICATOR:
1829b9e6d1fSgerardnico                        $result[Page::LOW_QUALITY_PAGE_INDICATOR] = $value;
183*5f891b7eSNickeau                        break;
1849b9e6d1fSgerardnico
185007225e5Sgerardnico                    // Canonical should be lowercase
186*5f891b7eSNickeau                    case Page::CANONICAL_PROPERTY:
18771f916b9Sgerardnico                        $result[Page::CANONICAL_PROPERTY] = $value;
188007225e5Sgerardnico                        $value = strtolower($value);
189*5f891b7eSNickeau                        break;
190007225e5Sgerardnico
191*5f891b7eSNickeau                }
192007225e5Sgerardnico                // Set the value persistently
193*5f891b7eSNickeau                p_set_metadata($ID, array($lowerCaseKey => $value));
194007225e5Sgerardnico
195007225e5Sgerardnico            }
196007225e5Sgerardnico
197007225e5Sgerardnico            $this->closeParsing($json);
198007225e5Sgerardnico
199*5f891b7eSNickeau            $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
200007225e5Sgerardnico
201007225e5Sgerardnico            return $result;
202007225e5Sgerardnico        }
203007225e5Sgerardnico
204007225e5Sgerardnico        return array();
205007225e5Sgerardnico    }
206007225e5Sgerardnico
207007225e5Sgerardnico    /**
208007225e5Sgerardnico     * Render the output
209007225e5Sgerardnico     * @param string $format
210007225e5Sgerardnico     * @param Doku_Renderer $renderer
211007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
212007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
213007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
214007225e5Sgerardnico     *
215007225e5Sgerardnico     *
216007225e5Sgerardnico     */
217007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
218007225e5Sgerardnico    {
219007225e5Sgerardnico
220007225e5Sgerardnico        switch ($format) {
221007225e5Sgerardnico            case 'xhtml':
222007225e5Sgerardnico                global $ID;
223007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
224*5f891b7eSNickeau                $state = $data[self::STATUS];
225007225e5Sgerardnico                if ($state == self::PARSING_STATE_ERROR) {
226*5f891b7eSNickeau                    $json = $data[PluginUtility::PAYLOAD];
227*5f891b7eSNickeau                    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);
228007225e5Sgerardnico                }
229007225e5Sgerardnico                break;
230007225e5Sgerardnico            case Analytics::RENDERER_FORMAT:
231007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
232007225e5Sgerardnico                if (array_key_exists("description", $data)) {
233007225e5Sgerardnico                    $renderer->setMeta("description", $data["description"]);
234007225e5Sgerardnico                }
23571f916b9Sgerardnico                if (array_key_exists(Page::CANONICAL_PROPERTY, $data)) {
23671f916b9Sgerardnico                    $renderer->setMeta(Page::CANONICAL_PROPERTY, $data[Page::CANONICAL_PROPERTY]);
237007225e5Sgerardnico                }
238f3748b38Sgerardnico                if (array_key_exists(Page::TITLE_PROPERTY, $data)) {
239f3748b38Sgerardnico                    $renderer->setMeta(Page::TITLE_PROPERTY, $data[Page::TITLE_PROPERTY]);
240f3748b38Sgerardnico                }
2419b9e6d1fSgerardnico                if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $data)) {
2429b9e6d1fSgerardnico                    $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $data[Page::LOW_QUALITY_PAGE_INDICATOR]);
2439b9e6d1fSgerardnico                }
244007225e5Sgerardnico                break;
245007225e5Sgerardnico
246007225e5Sgerardnico        }
247007225e5Sgerardnico        return true;
248007225e5Sgerardnico    }
249007225e5Sgerardnico
250007225e5Sgerardnico    /**
251007225e5Sgerardnico     *
252007225e5Sgerardnico     * @param array $json - The Json
253007225e5Sgerardnico     * Delete the controlled meta that are no more present if they exists
254007225e5Sgerardnico     * @return bool
255007225e5Sgerardnico     */
256*5f891b7eSNickeau    public
257*5f891b7eSNickeau    function closeParsing(array $json = array())
258007225e5Sgerardnico    {
259007225e5Sgerardnico        global $ID;
260007225e5Sgerardnico
261007225e5Sgerardnico        /**
262007225e5Sgerardnico         * The managed meta with the exception of
263007225e5Sgerardnico         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
264007225e5Sgerardnico         * because it's already managed by dokuwiki in description['abstract']
265007225e5Sgerardnico         */
266007225e5Sgerardnico        $managedMeta = [
26771f916b9Sgerardnico            Page::CANONICAL_PROPERTY,
268007225e5Sgerardnico            action_plugin_combo_metatitle::TITLE_META_KEY,
269007225e5Sgerardnico            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER
270007225e5Sgerardnico        ];
271007225e5Sgerardnico        $meta = p_read_metadata($ID);
272007225e5Sgerardnico        foreach ($managedMeta as $metaKey) {
273007225e5Sgerardnico            if (!array_key_exists($metaKey, $json)) {
274007225e5Sgerardnico                if (isset($meta['persistent'][$metaKey])) {
275007225e5Sgerardnico                    unset($meta['persistent'][$metaKey]);
276007225e5Sgerardnico                }
277007225e5Sgerardnico            }
278007225e5Sgerardnico        }
279007225e5Sgerardnico        return p_save_metadata($ID, $meta);
280007225e5Sgerardnico    }
281007225e5Sgerardnico
282007225e5Sgerardnico
283007225e5Sgerardnico}
284007225e5Sgerardnico
285