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