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