xref: /plugin/combo/syntax/frontmatter.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1007225e5Sgerardnico<?php
2007225e5Sgerardnico/**
3007225e5Sgerardnico * Front Matter implementation to add metadata
4007225e5Sgerardnico *
5007225e5Sgerardnico *
6007225e5Sgerardnico * that enhance the metadata dokuwiki system
7007225e5Sgerardnico * https://www.dokuwiki.org/metadata
8007225e5Sgerardnico * that use the Dublin Core Standard
9007225e5Sgerardnico * http://dublincore.org/
10007225e5Sgerardnico * by adding the front matter markup specification
11007225e5Sgerardnico * https://gerardnico.com/markup/front-matter
12007225e5Sgerardnico *
13007225e5Sgerardnico * Inspiration
14007225e5Sgerardnico * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php
15007225e5Sgerardnico * https://www.dokuwiki.org/plugin:semantic
16007225e5Sgerardnico *
17007225e5Sgerardnico * See also structured plugin
18007225e5Sgerardnico * https://www.dokuwiki.org/plugin:data
19007225e5Sgerardnico * https://www.dokuwiki.org/plugin:struct
20007225e5Sgerardnico *
21007225e5Sgerardnico */
22007225e5Sgerardnico
23*04fd306cSNickeauuse ComboStrap\ExceptionBadArgument;
24*04fd306cSNickeauuse ComboStrap\ExceptionBadSyntax;
25*04fd306cSNickeauuse ComboStrap\ExceptionCompile;
26*04fd306cSNickeauuse ComboStrap\ExceptionNotFound;
27*04fd306cSNickeauuse ComboStrap\ExceptionRuntime;
28*04fd306cSNickeauuse ComboStrap\ExecutionContext;
29007225e5Sgerardnicouse ComboStrap\LogUtility;
30*04fd306cSNickeauuse ComboStrap\MarkupPath;
31*04fd306cSNickeauuse ComboStrap\MarkupRef;
32*04fd306cSNickeauuse ComboStrap\MediaMarkup;
33*04fd306cSNickeauuse ComboStrap\Meta\Api\Metadata;
34*04fd306cSNickeauuse ComboStrap\Meta\Api\MetadataSystem;
35*04fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore;
36c3437056SNickeauuse ComboStrap\MetadataFrontmatterStore;
37c3437056SNickeauuse ComboStrap\MetadataStoreTransfer;
38*04fd306cSNickeauuse ComboStrap\PageDescription;
39*04fd306cSNickeauuse ComboStrap\Meta\Field\PageImagePath;
40*04fd306cSNickeauuse ComboStrap\Meta\Field\PageImages;
41a6bf47aaSNickeauuse ComboStrap\PluginUtility;
42007225e5Sgerardnico
4337748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
44007225e5Sgerardnico
45007225e5Sgerardnico
46007225e5Sgerardnico/**
47007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
48007225e5Sgerardnico * need to inherit from this class
49d5303bc5Sgerardnico *
50d5303bc5Sgerardnico * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
51007225e5Sgerardnico */
52007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
53007225e5Sgerardnico{
54*04fd306cSNickeau
55*04fd306cSNickeau    const PARSING_STATE_ERROR = 1;
56*04fd306cSNickeau    const PARSING_STATE_SUCCESSFUL = 0;
57*04fd306cSNickeau
585f891b7eSNickeau    const CANONICAL = "frontmatter";
59*04fd306cSNickeau    const TAG = "frontmatter";
60c3437056SNickeau    const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT = 0;
61007225e5Sgerardnico
62007225e5Sgerardnico    /**
6337748cd8SNickeau     * Used in the move plugin
6437748cd8SNickeau     * !!! The two last word of the plugin class !!!
6537748cd8SNickeau     */
6637748cd8SNickeau    const COMPONENT = 'combo_' . self::CANONICAL;
6737748cd8SNickeau    const START_TAG = '---json';
6837748cd8SNickeau    const END_TAG = '---';
6937748cd8SNickeau    const METADATA_IMAGE_CANONICAL = "metadata:image";
70c3437056SNickeau    const PATTERN = self::START_TAG . '.*?' . self::END_TAG;
7137748cd8SNickeau
7237748cd8SNickeau    /**
73c3437056SNickeau     * The update status for the update of the frontmatter
7437748cd8SNickeau     */
75c3437056SNickeau    const UPDATE_EXIT_CODE_DONE = 000;
76c3437056SNickeau    const UPDATE_EXIT_CODE_NOT_ENABLED = 100;
77c3437056SNickeau    const UPDATE_EXIT_CODE_NOT_CHANGED = 200;
78c3437056SNickeau    const UPDATE_EXIT_CODE_ERROR = 500;
7937748cd8SNickeau
8037748cd8SNickeau
8137748cd8SNickeau    /**
82007225e5Sgerardnico     * Syntax Type.
83007225e5Sgerardnico     *
84007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
85007225e5Sgerardnico     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
86007225e5Sgerardnico     *
87007225e5Sgerardnico     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
88007225e5Sgerardnico     *
89007225e5Sgerardnico     * baseonly - run only in the base
90007225e5Sgerardnico     */
91c3437056SNickeau    function getType(): string
92007225e5Sgerardnico    {
93007225e5Sgerardnico        return 'baseonly';
94007225e5Sgerardnico    }
95007225e5Sgerardnico
96*04fd306cSNickeau    public function getPType(): string
97531e725cSNickeau    {
9885e82846SNickeau        /**
9985e82846SNickeau         * This element create a section
10085e82846SNickeau         * element that is a div
10185e82846SNickeau         * that should not be in paragraph
10285e82846SNickeau         *
10385e82846SNickeau         * We make it a block
10485e82846SNickeau         */
10585e82846SNickeau        return "block";
106531e725cSNickeau    }
107531e725cSNickeau
108531e725cSNickeau
109007225e5Sgerardnico    /**
110007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
111007225e5Sgerardnico     * Higher number than the teaser-columns
112007225e5Sgerardnico     * because the mode with the lowest sort number will win out
113007225e5Sgerardnico     */
114007225e5Sgerardnico    function getSort()
115007225e5Sgerardnico    {
116007225e5Sgerardnico        return 99;
117007225e5Sgerardnico    }
118007225e5Sgerardnico
119007225e5Sgerardnico    /**
120007225e5Sgerardnico     * Create a pattern that will called this plugin
121007225e5Sgerardnico     *
122007225e5Sgerardnico     * @param string $mode
123007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
124007225e5Sgerardnico     */
125007225e5Sgerardnico    function connectTo($mode)
126007225e5Sgerardnico    {
127007225e5Sgerardnico        if ($mode == "base") {
128007225e5Sgerardnico            // only from the top
129c3437056SNickeau            $this->Lexer->addSpecialPattern(self::PATTERN, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
130007225e5Sgerardnico        }
131007225e5Sgerardnico    }
132007225e5Sgerardnico
133007225e5Sgerardnico    /**
134007225e5Sgerardnico     *
135007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
136007225e5Sgerardnico     * and to return the result for use in the renderer
137007225e5Sgerardnico     * This result is always cached until the page is modified.
138007225e5Sgerardnico     * @param string $match
139007225e5Sgerardnico     * @param int $state
140007225e5Sgerardnico     * @param int $pos
141007225e5Sgerardnico     * @param Doku_Handler $handler
142*04fd306cSNickeau     * @return array
143007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
144007225e5Sgerardnico     *
145007225e5Sgerardnico     */
146*04fd306cSNickeau    function handle($match, $state, $pos, Doku_Handler $handler): array
147007225e5Sgerardnico    {
148007225e5Sgerardnico
149a6bf47aaSNickeau        $result = [];
150*04fd306cSNickeau
151c3437056SNickeau        try {
152*04fd306cSNickeau            $wikiPath = ExecutionContext::getActualOrCreateFromEnv()->getExecutingWikiPath();
153*04fd306cSNickeau            $parsedPage = MarkupPath::createPageFromPathObject($wikiPath);
154*04fd306cSNickeau        } catch (ExceptionCompile $e) {
155*04fd306cSNickeau            LogUtility::error("The global ID is unknown, we couldn't get the requested page", self::CANONICAL);
156*04fd306cSNickeau            return [];
157*04fd306cSNickeau        }
158*04fd306cSNickeau        try {
159*04fd306cSNickeau
160*04fd306cSNickeau            $frontMatterStore = MetadataFrontmatterStore::createFromFrontmatterString($parsedPage, $match);
161*04fd306cSNickeau            $result[PluginUtility::EXIT_CODE] = self::PARSING_STATE_SUCCESSFUL;
162*04fd306cSNickeau        } catch (ExceptionCompile $e) {
163c3437056SNickeau            // Decode problem
164*04fd306cSNickeau            $result[PluginUtility::EXIT_CODE] = self::PARSING_STATE_ERROR;
165*04fd306cSNickeau            $result[PluginUtility::EXIT_MESSAGE] = $match;
166c3437056SNickeau            return $result;
167c3437056SNickeau        }
16837748cd8SNickeau
16937748cd8SNickeau
170*04fd306cSNickeau        $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($parsedPage);
171*04fd306cSNickeau        $frontMatterData = $frontMatterStore->getData();
172c3437056SNickeau
173*04fd306cSNickeau        $transfer = MetadataStoreTransfer::createForPage($parsedPage)
174c3437056SNickeau            ->fromStore($frontMatterStore)
175c3437056SNickeau            ->toStore($targetStore)
176*04fd306cSNickeau            ->setMetadatas($frontMatterData)
177*04fd306cSNickeau            ->validate();
178c3437056SNickeau
179c3437056SNickeau        $messages = $transfer->getMessages();
180*04fd306cSNickeau        $validatedMetadatas = $transfer->getValidatedMetadatas();
181*04fd306cSNickeau        $renderMetadata = [];
182*04fd306cSNickeau        foreach ($validatedMetadatas as $metadataObject) {
183*04fd306cSNickeau            $renderMetadata[$metadataObject::getPersistentName()] = $metadataObject->toStoreValue();
1841fa8c418SNickeau        }
185c3437056SNickeau
186c3437056SNickeau        foreach ($messages as $message) {
187*04fd306cSNickeau            $message->sendToLogUtility();
1881fa8c418SNickeau        }
1891fa8c418SNickeau
190c3437056SNickeau        /**
191c3437056SNickeau         * Return them for metadata rendering
192c3437056SNickeau         */
193*04fd306cSNickeau        $result[PluginUtility::ATTRIBUTES] = $renderMetadata;
194007225e5Sgerardnico        return $result;
195007225e5Sgerardnico
196007225e5Sgerardnico    }
197007225e5Sgerardnico
198007225e5Sgerardnico    /**
199007225e5Sgerardnico     * Render the output
200007225e5Sgerardnico     * @param string $format
201007225e5Sgerardnico     * @param Doku_Renderer $renderer
202007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
203007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
204007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
205007225e5Sgerardnico     *
206007225e5Sgerardnico     *
207007225e5Sgerardnico     */
208c3437056SNickeau    function render($format, Doku_Renderer $renderer, $data): bool
209007225e5Sgerardnico    {
210007225e5Sgerardnico
211*04fd306cSNickeau        try {
212*04fd306cSNickeau            $executingPath = ExecutionContext::getActualOrCreateFromEnv()
213*04fd306cSNickeau                ->getExecutingWikiPath();
214*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
215*04fd306cSNickeau            // markup string rendering
216*04fd306cSNickeau            return false;
217*04fd306cSNickeau        }
218007225e5Sgerardnico        switch ($format) {
219007225e5Sgerardnico            case 'xhtml':
220*04fd306cSNickeau
221007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
222*04fd306cSNickeau                $exitCode = $data[PluginUtility::EXIT_CODE];
223*04fd306cSNickeau                if ($exitCode == self::PARSING_STATE_ERROR) {
224*04fd306cSNickeau                    $json = MetadataFrontmatterStore::stripFrontmatterTag($data[PluginUtility::EXIT_MESSAGE]);
225*04fd306cSNickeau                    LogUtility::error("Front Matter: The json object for the page ($executingPath) is not valid. " . \ComboStrap\Json::getValidationLink($json), self::CANONICAL);
226007225e5Sgerardnico                }
227*04fd306cSNickeau                return true;
228c3437056SNickeau
229531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
230a6bf47aaSNickeau
231*04fd306cSNickeau                if ($data[PluginUtility::EXIT_CODE] !== self::PARSING_STATE_SUCCESSFUL) {
232*04fd306cSNickeau                    return true;
233a6bf47aaSNickeau                }
234a6bf47aaSNickeau
235007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
236c3437056SNickeau                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
237c3437056SNickeau                foreach ($frontMatterJsonArray as $key => $value) {
23837748cd8SNickeau
239*04fd306cSNickeau                    /**
240*04fd306cSNickeau                     * Hack while metadata and analtyics stats are not together
241*04fd306cSNickeau                     */
242*04fd306cSNickeau                    if ($key === PageDescription::DESCRIPTION_PROPERTY) {
243*04fd306cSNickeau                        $value = $value['abstract'];
244*04fd306cSNickeau                    }
245c3437056SNickeau                    $renderer->setAnalyticsMetaForReporting($key, $value);
246c3437056SNickeau                    if ($key === PageImages::PROPERTY_NAME) {
24737748cd8SNickeau                        $this->updateImageStatistics($value, $renderer);
248007225e5Sgerardnico                    }
24937748cd8SNickeau
2509b9e6d1fSgerardnico                }
251*04fd306cSNickeau                return true;
25237748cd8SNickeau
253a6bf47aaSNickeau            case "metadata":
254a6bf47aaSNickeau
255*04fd306cSNickeau
25637748cd8SNickeau                /** @var Doku_Renderer_metadata $renderer */
257*04fd306cSNickeau                if ($data[PluginUtility::EXIT_CODE] === self::PARSING_STATE_ERROR) {
258*04fd306cSNickeau                    if (PluginUtility::isTest()) {
259c3437056SNickeau                        // fail if test
260*04fd306cSNickeau                        throw new ExceptionRuntime("Front Matter: The json object for the page () is not valid.", LogUtility::LVL_MSG_ERROR);
261c3437056SNickeau                    }
262a6bf47aaSNickeau                    return false;
263a6bf47aaSNickeau                }
264a6bf47aaSNickeau
265c3437056SNickeau                /**
266*04fd306cSNickeau                 * Empty string
267*04fd306cSNickeau                 * Rare case, we delete all mutable meta if present
268*04fd306cSNickeau                 */
269*04fd306cSNickeau                $frontmatterData = $data[PluginUtility::ATTRIBUTES];
270*04fd306cSNickeau                if (sizeof($frontmatterData) === 0) {
271*04fd306cSNickeau                    foreach (MetadataSystem::getMutableMetadata() as $metaData) {
272*04fd306cSNickeau                        $metaKey = $metaData::getName();
273*04fd306cSNickeau                        if ($metaKey === PageDescription::PROPERTY_NAME) {
274*04fd306cSNickeau                            // array
275*04fd306cSNickeau                            continue;
276*04fd306cSNickeau                        }
277*04fd306cSNickeau                        // runtime
278*04fd306cSNickeau                        if ($renderer->meta[$metaKey]) {
279*04fd306cSNickeau                            unset($renderer->meta[$metaKey]);
280*04fd306cSNickeau                        }
281*04fd306cSNickeau                        // persistent
282*04fd306cSNickeau                        if ($renderer->persistent[$metaKey]) {
283*04fd306cSNickeau                            unset($renderer->persistent[$metaKey]);
284*04fd306cSNickeau                        }
285*04fd306cSNickeau                    }
286*04fd306cSNickeau                    return true;
287*04fd306cSNickeau                }
288*04fd306cSNickeau
289*04fd306cSNickeau                /**
290*04fd306cSNickeau                 * Meta update
291*04fd306cSNickeau                 * (The {@link p_get_metadata()} starts {@link p_render_metadata()}
292*04fd306cSNickeau                 * and stores them if there is any diff
293*04fd306cSNickeau                 */
294*04fd306cSNickeau                foreach ($frontmatterData as $metaKey => $metaValue) {
295*04fd306cSNickeau
296*04fd306cSNickeau                    $renderer->meta[$metaKey] = $metaValue;
297*04fd306cSNickeau
298*04fd306cSNickeau                    /**
299*04fd306cSNickeau                     * Persistence is just a duplicate of the meta (ie current)
300*04fd306cSNickeau                     *
301*04fd306cSNickeau                     * Why from https://www.dokuwiki.org/devel:metadata#metadata_persistence
302*04fd306cSNickeau                     * The persistent array holds ****duplicates****
303*04fd306cSNickeau                     * as the {@link p_get_metadata()} returns only `current` data
304*04fd306cSNickeau                     * which should not be cleared during the rendering process.
305*04fd306cSNickeau                     */
306*04fd306cSNickeau                    $renderer->persistent[$metaKey] = $metaValue;
307*04fd306cSNickeau
308*04fd306cSNickeau                }
309*04fd306cSNickeau
310*04fd306cSNickeau
311*04fd306cSNickeau                /**
312c3437056SNickeau                 * Register media in index
313c3437056SNickeau                 */
314c3437056SNickeau                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
315c3437056SNickeau                if (isset($frontMatterJsonArray[PageImages::getPersistentName()])) {
316c3437056SNickeau                    $value = $frontMatterJsonArray[PageImages::getPersistentName()];
317a6bf47aaSNickeau
318c3437056SNickeau                    /**
319c3437056SNickeau                     * @var PageImages $pageImages
320c3437056SNickeau                     */
321*04fd306cSNickeau                    $page = MarkupPath::createPageFromPathObject($executingPath);
322c3437056SNickeau                    $pageImages = PageImages::createForPage($page)
323*04fd306cSNickeau                        ->setFromStoreValueWithoutException($value);
3240e43c1dbSgerardnico                    $pageImagesObject = $pageImages->getValueAsPageImages();
3250e43c1dbSgerardnico                    foreach ($pageImagesObject as $imageValue) {
326*04fd306cSNickeau                        $dokuwikiId = $imageValue->getImagePath()->getWikiId();
327*04fd306cSNickeau                        $attributes = [MarkupRef::REF_ATTRIBUTE => ":$dokuwikiId"];
328*04fd306cSNickeau                        try {
32937748cd8SNickeau                            syntax_plugin_combo_media::registerImageMeta($attributes, $renderer);
330*04fd306cSNickeau                        } catch (\Exception $e) {
331*04fd306cSNickeau                            LogUtility::internalError("The image registration did not work. Error: {$e->getMessage()}");
33237748cd8SNickeau                        }
333a6bf47aaSNickeau                    }
334*04fd306cSNickeau                }
335a6bf47aaSNickeau                break;
336007225e5Sgerardnico
337007225e5Sgerardnico        }
338007225e5Sgerardnico        return true;
339007225e5Sgerardnico    }
340007225e5Sgerardnico
341007225e5Sgerardnico
34237748cd8SNickeau    private function updateImageStatistics($value, $renderer)
34337748cd8SNickeau    {
344c3437056SNickeau        if (is_array($value) && sizeof($value) > 0) {
345c3437056SNickeau            $firstKey = array_keys($value)[0];
346c3437056SNickeau            if (is_numeric($firstKey)) {
34737748cd8SNickeau                foreach ($value as $subImage) {
34837748cd8SNickeau                    $this->updateImageStatistics($subImage, $renderer);
34937748cd8SNickeau                }
350c3437056SNickeau                return;
35137748cd8SNickeau            }
35237748cd8SNickeau        }
35337748cd8SNickeau
354c3437056SNickeau        /**
355c3437056SNickeau         * Code below is fucked up
356c3437056SNickeau         */
357c3437056SNickeau        $path = $value;
358c3437056SNickeau        if (is_array($value) && isset($value[PageImagePath::getPersistentName()])) {
359c3437056SNickeau            $path = $value[PageImagePath::getPersistentName()];
36037748cd8SNickeau        }
361*04fd306cSNickeau        try {
362*04fd306cSNickeau            $media = MediaMarkup::createFromRef($path);
363*04fd306cSNickeau        } catch (ExceptionBadArgument|ExceptionNotFound|ExceptionBadSyntax $e) {
364*04fd306cSNickeau            LogUtility::internalError("The media image statistics could not be created. The media markup could not be instantiated with the path ($path). Error:{$e->getMessage()}");
365*04fd306cSNickeau            return;
366*04fd306cSNickeau        }
367*04fd306cSNickeau
368c3437056SNickeau        $attributes = $media->toCallStackArray();
369c3437056SNickeau        syntax_plugin_combo_media::updateStatistics($attributes, $renderer);
370c3437056SNickeau
37137748cd8SNickeau    }
37237748cd8SNickeau
373007225e5Sgerardnico
374007225e5Sgerardnico}
375007225e5Sgerardnico
376