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