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