xref: /plugin/visualindex/action/prosemirror.php (revision 3c9c7f3beeea1dce712c368cb507b309c63f5d06)
1<?php
2
3use dokuwiki\plugin\visualindex\parser\VisualIndexNode;
4use dokuwiki\plugin\prosemirror\schema\Node;
5
6class action_plugin_visualindex_prosemirror extends \dokuwiki\Extension\ActionPlugin
7{
8    public function register(Doku_Event_Handler $controller)
9    {
10        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'addJsInfo');
11        $controller->register_hook('PROSEMIRROR_RENDER_PLUGIN', 'BEFORE', $this, 'handleRender');
12        $controller->register_hook('PROSEMIRROR_PARSE_UNKNOWN', 'BEFORE', $this, 'handleParseUnknown');
13        $controller->register_hook('PLUGIN_PAGESICON_UPDATED', 'AFTER', $this, 'handlePagesiconUpdated');
14    }
15
16    private function namespaceDir(string $namespace): string
17    {
18        global $conf;
19        return rtrim((string)$conf['datadir'], '/') . '/' . utf8_encodeFN(str_replace(':', '/', $namespace));
20    }
21
22    private function getPageNamespaceInfo(string $namespace): array
23    {
24        $namespaces = explode(':', $namespace);
25        $pageID = array_pop($namespaces);
26        $parentNamespace = implode(':', $namespaces);
27        $parentID = '';
28        if ($parentNamespace !== '') {
29            $parts = explode(':', $parentNamespace);
30            $parentID = (string)array_pop($parts);
31        }
32        return [
33            'pageID' => $pageID,
34            'parentNamespace' => $parentNamespace,
35            'parentID' => $parentID,
36        ];
37    }
38
39    private function isHomepage(string $pageID, string $parentID): bool
40    {
41        global $conf;
42        $startPageID = (string)$conf['start'];
43        return $pageID === $startPageID || ($parentID !== '' && $pageID === $parentID);
44    }
45
46    private function getCurrentNamespace(string $hostPageID): string
47    {
48        if (!is_dir($this->namespaceDir($hostPageID))) {
49            $info = $this->getPageNamespaceInfo($hostPageID);
50            if ($this->isHomepage((string)$info['pageID'], (string)$info['parentID'])) {
51                return (string)$info['parentNamespace'];
52            }
53        }
54        return $hostPageID;
55    }
56
57    private function resolveNamespaceExpression(string $expr, string $hostPageID): string
58    {
59        $expr = trim($expr);
60        if ($expr === '.') return $this->getCurrentNamespace($hostPageID);
61        if ($expr !== '' && $expr[0] === '~') {
62            $rel = cleanID(ltrim($expr, '~'));
63            $base = $this->getCurrentNamespace($hostPageID);
64            return cleanID($base !== '' ? ($base . ':' . $rel) : $rel);
65        }
66        return cleanID($expr);
67    }
68
69    private function isTargetInNamespace(string $targetPage, string $namespace): bool
70    {
71        if ($namespace === '') return true;
72        $targetPage = cleanID($targetPage);
73        $namespace = cleanID($namespace);
74        if ($targetPage === '' || $namespace === '') return false;
75        return ($targetPage === $namespace) || (strpos($targetPage . ':', $namespace . ':') === 0);
76    }
77
78    private function pageUsesAffectedVisualindex(string $hostPageID, string $content, string $targetPage): bool
79    {
80        if (!preg_match_all('/\{\{visualindex>(.*?)\}\}/i', $content, $matches, PREG_SET_ORDER)) return false;
81        foreach ($matches as $match) {
82            $raw = (string)($match[1] ?? '');
83            $parts = explode(';', $raw);
84            $namespaceExpr = trim((string)array_shift($parts));
85            $resolvedNS = $this->resolveNamespaceExpression($namespaceExpr, $hostPageID);
86            if ($this->isTargetInNamespace($targetPage, $resolvedNS)) {
87                return true;
88            }
89        }
90        return false;
91    }
92
93    private function invalidateCacheForTarget(string $needle): void
94    {
95        global $conf;
96        $datadir = rtrim((string)$conf['datadir'], '/');
97        if ($datadir === '' || !is_dir($datadir)) return;
98
99        $it = new RecursiveIteratorIterator(
100            new RecursiveDirectoryIterator($datadir, FilesystemIterator::SKIP_DOTS)
101        );
102        foreach ($it as $fileinfo) {
103            /** @var SplFileInfo $fileinfo */
104            if (!$fileinfo->isFile()) continue;
105            if (substr($fileinfo->getFilename(), -4) !== '.txt') continue;
106
107            $path = $fileinfo->getPathname();
108            $content = @file_get_contents($path);
109            if ($content === false || strpos($content, $needle) === false) continue;
110
111            $id = pathID($path);
112            if ($id === '') continue;
113            $cache = new \dokuwiki\Cache\CacheRenderer($id, wikiFN($id), 'xhtml');
114            $cache->removeCache();
115        }
116    }
117
118    public function addJsInfo(Doku_Event $event)
119    {
120        global $JSINFO;
121        if (!isset($JSINFO['plugins'])) $JSINFO['plugins'] = [];
122        if (!isset($JSINFO['plugins']['visualindex'])) $JSINFO['plugins']['visualindex'] = [];
123        $JSINFO['plugins']['visualindex']['show_in_editor_menu'] = (bool)$this->getConf('show_in_editor_menu');
124    }
125
126    public function handleRender(Doku_Event $event)
127    {
128        $data = $event->data;
129        if ($data['name'] !== 'visualindex_visualindex') return;
130
131        $event->preventDefault();
132        $event->stopPropagation();
133
134        $syntax = trim((string)$data['match']);
135        if ($syntax === '') {
136            $syntax = '{{visualindex>.}}';
137        }
138
139        $node = new Node('dwplugin_block');
140        $node->attr('class', 'dwplugin');
141        $node->attr('data-pluginname', 'visualindex');
142
143        $textNode = new Node('text');
144        $textNode->setText($syntax);
145        $node->addChild($textNode);
146
147        $data['renderer']->addToNodestack($node);
148    }
149
150    public function handleParseUnknown(Doku_Event $event)
151    {
152        if (($event->data['node']['type'] ?? '') !== 'visualindex') return;
153
154        $event->data['newNode'] = new VisualIndexNode($event->data['node'], $event->data['parent']);
155        $event->preventDefault();
156        $event->stopPropagation();
157    }
158
159    public function handlePagesiconUpdated(Doku_Event $event): void
160    {
161        $this->invalidateCacheForTarget('{{visualindex>');
162    }
163}
164