xref: /template/strap/syntax/frontmatter.php (revision 21913ab3235d516e2fa19c7e3929b555b3a2bda1)
1<?php
2/**
3 * Front Matter implementation to add metadata
4 *
5 *
6 * that enhance the metadata dokuwiki system
7 * https://www.dokuwiki.org/metadata
8 * that use the Dublin Core Standard
9 * http://dublincore.org/
10 * by adding the front matter markup specification
11 * https://gerardnico.com/markup/front-matter
12 *
13 * Inspiration
14 * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php
15 * https://www.dokuwiki.org/plugin:semantic
16 *
17 * See also structured plugin
18 * https://www.dokuwiki.org/plugin:data
19 * https://www.dokuwiki.org/plugin:struct
20 *
21 */
22
23use ComboStrap\Analytics;
24use ComboStrap\LogUtility;
25use ComboStrap\PluginUtility;
26use ComboStrap\Page;
27
28require_once(__DIR__ . '/../class/Analytics.php');
29
30if (!defined('DOKU_INC')) {
31    die();
32}
33
34/**
35 * All DokuWiki plugins to extend the parser/rendering mechanism
36 * need to inherit from this class
37 *
38 * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
39 */
40class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
41{
42    const PARSING_STATE_EMPTY = "empty";
43    const PARSING_STATE_ERROR = "error";
44    const PARSING_STATE_SUCCESSFUL = "successful";
45    const STATUS = "status";
46    const CANONICAL = "frontmatter";
47    const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing';
48
49    /**
50     * Syntax Type.
51     *
52     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
53     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
54     *
55     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
56     *
57     * baseonly - run only in the base
58     */
59    function getType()
60    {
61        return 'baseonly';
62    }
63
64    /**
65     * @see Doku_Parser_Mode::getSort()
66     * Higher number than the teaser-columns
67     * because the mode with the lowest sort number will win out
68     */
69    function getSort()
70    {
71        return 99;
72    }
73
74    /**
75     * Create a pattern that will called this plugin
76     *
77     * @param string $mode
78     * @see Doku_Parser_Mode::connectTo()
79     */
80    function connectTo($mode)
81    {
82        if ($mode == "base") {
83            // only from the top
84            $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent()));
85        }
86    }
87
88    /**
89     *
90     * The handle function goal is to parse the matched syntax through the pattern function
91     * and to return the result for use in the renderer
92     * This result is always cached until the page is modified.
93     * @param string $match
94     * @param int $state
95     * @param int $pos
96     * @param Doku_Handler $handler
97     * @return array|bool
98     * @see DokuWiki_Syntax_Plugin::handle()
99     *
100     */
101    function handle($match, $state, $pos, Doku_Handler $handler)
102    {
103
104        if ($state == DOKU_LEXER_SPECIAL) {
105
106            global $ID;
107
108            // strip
109            //   from start `---json` + eol = 8
110            //   from end   `---` + eol = 4
111            $match = substr($match, 7, -3);
112
113            // Empty front matter
114            if (trim($match) == "") {
115                $this->closeParsing();
116                return array(self::STATUS => self::PARSING_STATE_EMPTY);
117            }
118
119            // Otherwise you get an object ie $arrayFormat-> syntax
120            $arrayFormat = true;
121            $json = json_decode($match, $arrayFormat);
122
123            // Decodage problem
124            if ($json == null) {
125                return array(
126                    self::STATUS => self::PARSING_STATE_ERROR,
127                    PluginUtility::PAYLOAD => $match
128                );
129            }
130
131            $notModifiableMeta = [
132                "date",
133                "user",
134                "last_change",
135                "creator",
136                "contributor"
137            ];
138            $result = array();
139            foreach ($json as $key => $value) {
140
141                $lowerCaseKey = trim(strtolower($key));
142
143                // Not modifiable metadata
144                if (in_array($lowerCaseKey, $notModifiableMeta)) {
145                    LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
146                    continue;
147                }
148
149                switch ($lowerCaseKey) {
150
151                    case Page::DESCRIPTION_PROPERTY:
152                        $result["description"] = $value;
153                        /**
154                         * Overwrite also the actual description
155                         */
156                        p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
157                            "abstract" => $value,
158                            "origin" => syntax_plugin_combo_frontmatter::CANONICAL
159                        )));
160                        /**
161                         * Continue because
162                         * the description value was already stored
163                         * We don't want to override it
164                         * And continue 2 because continue == break in a switch
165                         */
166                        continue 2;
167                        break;
168
169                    /**
170                     * Pass the title to the metadata
171                     * to advertise that it's in the front-matter
172                     * for the quality rules
173                     */
174                    case Page::TITLE_PROPERTY:
175                        $result[Page::TITLE_PROPERTY] = $value;
176                        break;
177
178                    /**
179                     * Pass the low quality indicator
180                     * to advertise that it's in the front-matter
181                     */
182                    case Page::LOW_QUALITY_PAGE_INDICATOR:
183                        $result[Page::LOW_QUALITY_PAGE_INDICATOR] = $value;
184                        break;
185
186                    // Canonical should be lowercase
187                    case Page::CANONICAL_PROPERTY:
188                        $result[Page::CANONICAL_PROPERTY] = $value;
189                        $value = strtolower($value);
190                        break;
191
192                }
193                // Set the value persistently
194                p_set_metadata($ID, array($lowerCaseKey => $value));
195
196            }
197
198            $this->closeParsing($json);
199
200            $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
201            $result[PluginUtility::POSITION]=[$pos,$pos + strlen($match) + 1];
202
203            return $result;
204        }
205
206        return array();
207    }
208
209    /**
210     * Render the output
211     * @param string $format
212     * @param Doku_Renderer $renderer
213     * @param array $data - what the function handle() return'ed
214     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
215     * @see DokuWiki_Syntax_Plugin::render()
216     *
217     *
218     */
219    function render($format, Doku_Renderer $renderer, $data)
220    {
221
222        switch ($format) {
223            case 'xhtml':
224                global $ID;
225                /** @var Doku_Renderer_xhtml $renderer */
226
227                $state = $data[self::STATUS];
228                if ($state == self::PARSING_STATE_ERROR) {
229                    $json = $data[PluginUtility::PAYLOAD];
230                    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);
231                }
232
233                /**
234                 * Section
235                 */
236                list($startPosition,$endPosition) =  $data[PluginUtility::POSITION];
237                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING,1)) {
238                    $position = $startPosition;
239                    $name = self::CANONICAL;
240                    PluginUtility::startSection($renderer, $position, $name);
241                    $renderer->finishSectionEdit($endPosition);
242                }
243                break;
244            case Analytics::RENDERER_FORMAT:
245                /** @var renderer_plugin_combo_analytics $renderer */
246                if (array_key_exists("description", $data)) {
247                    $renderer->setMeta("description", $data["description"]);
248                }
249                if (array_key_exists(Page::CANONICAL_PROPERTY, $data)) {
250                    $renderer->setMeta(Page::CANONICAL_PROPERTY, $data[Page::CANONICAL_PROPERTY]);
251                }
252                if (array_key_exists(Page::TITLE_PROPERTY, $data)) {
253                    $renderer->setMeta(Page::TITLE_PROPERTY, $data[Page::TITLE_PROPERTY]);
254                }
255                if (array_key_exists(Page::LOW_QUALITY_PAGE_INDICATOR, $data)) {
256                    $renderer->setMeta(Page::LOW_QUALITY_PAGE_INDICATOR, $data[Page::LOW_QUALITY_PAGE_INDICATOR]);
257                }
258                break;
259
260        }
261        return true;
262    }
263
264    /**
265     *
266     * @param array $json - The Json
267     * Delete the controlled meta that are no more present if they exists
268     * @return bool
269     */
270    public
271    function closeParsing(array $json = array())
272    {
273        global $ID;
274
275        /**
276         * The managed meta with the exception of
277         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
278         * because it's already managed by dokuwiki in description['abstract']
279         */
280        $managedMeta = [
281            Page::CANONICAL_PROPERTY,
282            action_plugin_combo_metatitle::TITLE_META_KEY,
283            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER
284        ];
285        $meta = p_read_metadata($ID);
286        foreach ($managedMeta as $metaKey) {
287            if (!array_key_exists($metaKey, $json)) {
288                if (isset($meta['persistent'][$metaKey])) {
289                    unset($meta['persistent'][$metaKey]);
290                }
291            }
292        }
293        return p_save_metadata($ID, $meta);
294    }
295
296
297}
298
299