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