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