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