xref: /plugin/combo/syntax/frontmatter.php (revision 007225e5fb2d3f64edaccd3bd447ca26effb9d68)
1*007225e5Sgerardnico<?php
2*007225e5Sgerardnico/**
3*007225e5Sgerardnico * Front Matter implementation to add metadata
4*007225e5Sgerardnico *
5*007225e5Sgerardnico *
6*007225e5Sgerardnico * that enhance the metadata dokuwiki system
7*007225e5Sgerardnico * https://www.dokuwiki.org/metadata
8*007225e5Sgerardnico * that use the Dublin Core Standard
9*007225e5Sgerardnico * http://dublincore.org/
10*007225e5Sgerardnico * by adding the front matter markup specification
11*007225e5Sgerardnico * https://gerardnico.com/markup/front-matter
12*007225e5Sgerardnico *
13*007225e5Sgerardnico * Inspiration
14*007225e5Sgerardnico * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php
15*007225e5Sgerardnico * https://www.dokuwiki.org/plugin:semantic
16*007225e5Sgerardnico *
17*007225e5Sgerardnico * See also structured plugin
18*007225e5Sgerardnico * https://www.dokuwiki.org/plugin:data
19*007225e5Sgerardnico * https://www.dokuwiki.org/plugin:struct
20*007225e5Sgerardnico *
21*007225e5Sgerardnico */
22*007225e5Sgerardnico
23*007225e5Sgerardnicouse ComboStrap\Analytics;
24*007225e5Sgerardnicouse ComboStrap\LogUtility;
25*007225e5Sgerardnicouse ComboStrap\PluginUtility;
26*007225e5Sgerardnicouse ComboStrap\UrlCanonical;
27*007225e5Sgerardnico
28*007225e5Sgerardnicorequire_once(__DIR__ . '/../class/Analytics.php');
29*007225e5Sgerardnico
30*007225e5Sgerardnicoif (!defined('DOKU_INC')) {
31*007225e5Sgerardnico    die();
32*007225e5Sgerardnico}
33*007225e5Sgerardnico
34*007225e5Sgerardnico/**
35*007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
36*007225e5Sgerardnico * need to inherit from this class
37*007225e5Sgerardnico */
38*007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
39*007225e5Sgerardnico{
40*007225e5Sgerardnico    const PARSING_STATE_EMPTY = "empty";
41*007225e5Sgerardnico    const PARSING_STATE_ERROR = "error";
42*007225e5Sgerardnico    const PARSING_STATE_SUCCESSFUL = "successful";
43*007225e5Sgerardnico
44*007225e5Sgerardnico    /**
45*007225e5Sgerardnico     * Syntax Type.
46*007225e5Sgerardnico     *
47*007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
48*007225e5Sgerardnico     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
49*007225e5Sgerardnico     *
50*007225e5Sgerardnico     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
51*007225e5Sgerardnico     *
52*007225e5Sgerardnico     * baseonly - run only in the base
53*007225e5Sgerardnico     */
54*007225e5Sgerardnico    function getType()
55*007225e5Sgerardnico    {
56*007225e5Sgerardnico        return 'baseonly';
57*007225e5Sgerardnico    }
58*007225e5Sgerardnico
59*007225e5Sgerardnico    /**
60*007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
61*007225e5Sgerardnico     * Higher number than the teaser-columns
62*007225e5Sgerardnico     * because the mode with the lowest sort number will win out
63*007225e5Sgerardnico     */
64*007225e5Sgerardnico    function getSort()
65*007225e5Sgerardnico    {
66*007225e5Sgerardnico        return 99;
67*007225e5Sgerardnico    }
68*007225e5Sgerardnico
69*007225e5Sgerardnico    /**
70*007225e5Sgerardnico     * Create a pattern that will called this plugin
71*007225e5Sgerardnico     *
72*007225e5Sgerardnico     * @param string $mode
73*007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
74*007225e5Sgerardnico     */
75*007225e5Sgerardnico    function connectTo($mode)
76*007225e5Sgerardnico    {
77*007225e5Sgerardnico        if ($mode == "base") {
78*007225e5Sgerardnico            // only from the top
79*007225e5Sgerardnico            $this->Lexer->addSpecialPattern('---json.*?---', $mode, PluginUtility::getModeForComponent($this->getPluginComponent()));
80*007225e5Sgerardnico        }
81*007225e5Sgerardnico    }
82*007225e5Sgerardnico
83*007225e5Sgerardnico    /**
84*007225e5Sgerardnico     *
85*007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
86*007225e5Sgerardnico     * and to return the result for use in the renderer
87*007225e5Sgerardnico     * This result is always cached until the page is modified.
88*007225e5Sgerardnico     * @param string $match
89*007225e5Sgerardnico     * @param int $state
90*007225e5Sgerardnico     * @param int $pos
91*007225e5Sgerardnico     * @param Doku_Handler $handler
92*007225e5Sgerardnico     * @return array|bool
93*007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
94*007225e5Sgerardnico     *
95*007225e5Sgerardnico     */
96*007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
97*007225e5Sgerardnico    {
98*007225e5Sgerardnico
99*007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
100*007225e5Sgerardnico
101*007225e5Sgerardnico            global $ID;
102*007225e5Sgerardnico
103*007225e5Sgerardnico            // strip
104*007225e5Sgerardnico            //   from start `---json` + eol = 8
105*007225e5Sgerardnico            //   from end   `---` + eol = 4
106*007225e5Sgerardnico            $match = substr($match, 7, -3);
107*007225e5Sgerardnico
108*007225e5Sgerardnico            // Empty front matter
109*007225e5Sgerardnico            if (trim($match) == "") {
110*007225e5Sgerardnico                $this->closeParsing();
111*007225e5Sgerardnico                return array("state" => self::PARSING_STATE_EMPTY);
112*007225e5Sgerardnico            }
113*007225e5Sgerardnico
114*007225e5Sgerardnico            // Otherwise you get an object ie $arrayFormat-> syntax
115*007225e5Sgerardnico            $arrayFormat = true;
116*007225e5Sgerardnico            $json = json_decode($match, $arrayFormat);
117*007225e5Sgerardnico
118*007225e5Sgerardnico            // Decodage problem
119*007225e5Sgerardnico            if ($json == null) {
120*007225e5Sgerardnico                return array("state" => self::PARSING_STATE_ERROR);
121*007225e5Sgerardnico            }
122*007225e5Sgerardnico
123*007225e5Sgerardnico            // Trim it
124*007225e5Sgerardnico            $jsonKey = array_map('trim', array_keys($json));
125*007225e5Sgerardnico            // We will get a php warning here because the values may be an array
126*007225e5Sgerardnico            // and trim accept only string
127*007225e5Sgerardnico            $oldLevel = error_reporting(E_ERROR);
128*007225e5Sgerardnico            $jsonValues = array_map('trim', $json);
129*007225e5Sgerardnico            error_reporting($oldLevel);
130*007225e5Sgerardnico            $json = array_combine($jsonKey, $jsonValues);
131*007225e5Sgerardnico
132*007225e5Sgerardnico
133*007225e5Sgerardnico            $notModifiableMeta = [
134*007225e5Sgerardnico                "date",
135*007225e5Sgerardnico                "user",
136*007225e5Sgerardnico                "last_change",
137*007225e5Sgerardnico                "creator",
138*007225e5Sgerardnico                "contributor"
139*007225e5Sgerardnico            ];
140*007225e5Sgerardnico            $result = array();
141*007225e5Sgerardnico            foreach ($json as $key => $value) {
142*007225e5Sgerardnico
143*007225e5Sgerardnico                // Not modifiable metadata
144*007225e5Sgerardnico                if (in_array($key, $notModifiableMeta)) {
145*007225e5Sgerardnico                    LogUtility::msg("Front Matter: The metadata ($key) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
146*007225e5Sgerardnico                    continue;
147*007225e5Sgerardnico                }
148*007225e5Sgerardnico
149*007225e5Sgerardnico                // Description is special
150*007225e5Sgerardnico                if ($key == "description") {
151*007225e5Sgerardnico                    $result["description"] = $value;
152*007225e5Sgerardnico                    p_set_metadata($ID, array("description" => array("abstract" => $value)));
153*007225e5Sgerardnico                    continue;
154*007225e5Sgerardnico                }
155*007225e5Sgerardnico
156*007225e5Sgerardnico                // Canonical should be lowercase
157*007225e5Sgerardnico                if ($key == UrlCanonical::CANONICAL_PROPERTY) {
158*007225e5Sgerardnico                    $result[UrlCanonical::CANONICAL_PROPERTY] = $value;
159*007225e5Sgerardnico                    $value = strtolower($value);
160*007225e5Sgerardnico                }
161*007225e5Sgerardnico
162*007225e5Sgerardnico                // Set the value persistently
163*007225e5Sgerardnico                p_set_metadata($ID, array($key => $value));
164*007225e5Sgerardnico
165*007225e5Sgerardnico            }
166*007225e5Sgerardnico
167*007225e5Sgerardnico            $this->closeParsing($json);
168*007225e5Sgerardnico
169*007225e5Sgerardnico            $result["state"]= self::PARSING_STATE_SUCCESSFUL;
170*007225e5Sgerardnico
171*007225e5Sgerardnico            return $result;
172*007225e5Sgerardnico        }
173*007225e5Sgerardnico
174*007225e5Sgerardnico        return array();
175*007225e5Sgerardnico    }
176*007225e5Sgerardnico
177*007225e5Sgerardnico    /**
178*007225e5Sgerardnico     * Render the output
179*007225e5Sgerardnico     * @param string $format
180*007225e5Sgerardnico     * @param Doku_Renderer $renderer
181*007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
182*007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
183*007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
184*007225e5Sgerardnico     *
185*007225e5Sgerardnico     *
186*007225e5Sgerardnico     */
187*007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
188*007225e5Sgerardnico    {
189*007225e5Sgerardnico        // TODO: https://developers.google.com/search/docs/data-types/breadcrumb#breadcrumb-list
190*007225e5Sgerardnico        // News article: https://developers.google.com/search/docs/data-types/article
191*007225e5Sgerardnico        // News article: https://developers.google.com/search/docs/data-types/paywalled-content
192*007225e5Sgerardnico        // What is ?: https://developers.google.com/search/docs/data-types/qapage
193*007225e5Sgerardnico        // How to ?: https://developers.google.com/search/docs/data-types/how-to
194*007225e5Sgerardnico
195*007225e5Sgerardnico        switch ($format) {
196*007225e5Sgerardnico            case 'xhtml':
197*007225e5Sgerardnico                global $ID;
198*007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
199*007225e5Sgerardnico                $state = $data["state"];
200*007225e5Sgerardnico                if ($state == self::PARSING_STATE_ERROR) {
201*007225e5Sgerardnico                    LogUtility::msg("Front Matter: The json object for the page ($ID) is not valid", LogUtility::LVL_MSG_ERROR);
202*007225e5Sgerardnico                }
203*007225e5Sgerardnico                break;
204*007225e5Sgerardnico            case Analytics::RENDERER_FORMAT:
205*007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
206*007225e5Sgerardnico                if (array_key_exists("description", $data)) {
207*007225e5Sgerardnico                    $renderer->setMeta("description", $data["description"]);
208*007225e5Sgerardnico                }
209*007225e5Sgerardnico                if (array_key_exists(UrlCanonical::CANONICAL_PROPERTY, $data)) {
210*007225e5Sgerardnico                    $renderer->setMeta(UrlCanonical::CANONICAL_PROPERTY, $data[UrlCanonical::CANONICAL_PROPERTY]);
211*007225e5Sgerardnico                }
212*007225e5Sgerardnico                break;
213*007225e5Sgerardnico
214*007225e5Sgerardnico        }
215*007225e5Sgerardnico        return true;
216*007225e5Sgerardnico    }
217*007225e5Sgerardnico
218*007225e5Sgerardnico    /**
219*007225e5Sgerardnico     *
220*007225e5Sgerardnico     * @param array $json - The Json
221*007225e5Sgerardnico     * Delete the controlled meta that are no more present if they exists
222*007225e5Sgerardnico     * @return bool
223*007225e5Sgerardnico     */
224*007225e5Sgerardnico    public function closeParsing(array $json = array())
225*007225e5Sgerardnico    {
226*007225e5Sgerardnico        global $ID;
227*007225e5Sgerardnico
228*007225e5Sgerardnico        /**
229*007225e5Sgerardnico         * The managed meta with the exception of
230*007225e5Sgerardnico         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
231*007225e5Sgerardnico         * because it's already managed by dokuwiki in description['abstract']
232*007225e5Sgerardnico         */
233*007225e5Sgerardnico        $managedMeta = [
234*007225e5Sgerardnico            UrlCanonical::CANONICAL_PROPERTY,
235*007225e5Sgerardnico            action_plugin_combo_metatitle::TITLE_META_KEY,
236*007225e5Sgerardnico            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER
237*007225e5Sgerardnico        ];
238*007225e5Sgerardnico        $meta = p_read_metadata($ID);
239*007225e5Sgerardnico        foreach ($managedMeta as $metaKey) {
240*007225e5Sgerardnico            if (!array_key_exists($metaKey, $json)) {
241*007225e5Sgerardnico                if (isset($meta['persistent'][$metaKey])) {
242*007225e5Sgerardnico                    unset($meta['persistent'][$metaKey]);
243*007225e5Sgerardnico                }
244*007225e5Sgerardnico            }
245*007225e5Sgerardnico        }
246*007225e5Sgerardnico        return p_save_metadata($ID, $meta);
247*007225e5Sgerardnico    }
248*007225e5Sgerardnico
249*007225e5Sgerardnico
250*007225e5Sgerardnico}
251*007225e5Sgerardnico
252