1<?php 2 3use dokuwiki\plugin\catmenu\parser\CatmenuNode; 4use dokuwiki\plugin\prosemirror\schema\Node; 5 6class action_plugin_catmenu_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 pageUsesAffectedCatmenu(string $hostPageID, string $content, string $targetPage): bool 79 { 80 if (!preg_match_all('/\{\{catmenu>(.*?)\}\}/i', $content, $matches, PREG_SET_ORDER)) return false; 81 foreach ($matches as $match) { 82 $namespaceExpr = (string)($match[1] ?? ''); 83 $resolvedNS = $this->resolveNamespaceExpression($namespaceExpr, $hostPageID); 84 if ($this->isTargetInNamespace($targetPage, $resolvedNS)) { 85 return true; 86 } 87 } 88 return false; 89 } 90 91 private function invalidateCacheForTarget(string $needle): void 92 { 93 global $conf; 94 $datadir = rtrim((string)$conf['datadir'], '/'); 95 if ($datadir === '' || !is_dir($datadir)) return; 96 97 $it = new RecursiveIteratorIterator( 98 new RecursiveDirectoryIterator($datadir, FilesystemIterator::SKIP_DOTS) 99 ); 100 foreach ($it as $fileinfo) { 101 /** @var SplFileInfo $fileinfo */ 102 if (!$fileinfo->isFile()) continue; 103 if (substr($fileinfo->getFilename(), -4) !== '.txt') continue; 104 105 $path = $fileinfo->getPathname(); 106 $content = @file_get_contents($path); 107 if ($content === false || strpos($content, $needle) === false) continue; 108 109 $id = pathID($path); 110 if ($id === '') continue; 111 $cache = new \dokuwiki\Cache\CacheRenderer($id, wikiFN($id), 'xhtml'); 112 $cache->removeCache(); 113 } 114 } 115 116 public function addJsInfo(Doku_Event $event) 117 { 118 global $ID; 119 global $JSINFO; 120 if (!isset($JSINFO['plugins'])) $JSINFO['plugins'] = []; 121 if (!isset($JSINFO['plugins']['catmenu'])) $JSINFO['plugins']['catmenu'] = []; 122 $JSINFO['plugins']['catmenu']['show_in_editor_menu'] = (bool)$this->getConf('show_in_editor_menu'); 123 124 $pagesiconHelper = plugin_load('helper', 'pagesicon'); 125 $JSINFO['plugins']['catmenu']['pagesicon_available'] = (bool)$pagesiconHelper; 126 if ($pagesiconHelper) { 127 $JSINFO['plugins']['catmenu']['pagesicon_upload_url'] = wl((string)$ID, ['do' => 'pagesicon']); 128 } 129 } 130 131 public function handleRender(Doku_Event $event) 132 { 133 $data = $event->data; 134 if (($data['name'] ?? '') !== 'catmenu_catmenu') return; 135 136 $event->preventDefault(); 137 $event->stopPropagation(); 138 139 $syntax = trim((string)($data['match'] ?? '')); 140 if ($syntax === '') { 141 $syntax = '{{catmenu>.}}'; 142 } 143 144 $node = new Node('dwplugin_block'); 145 $node->attr('class', 'dwplugin'); 146 $node->attr('data-pluginname', 'catmenu'); 147 148 $textNode = new Node('text'); 149 $textNode->setText($syntax); 150 $node->addChild($textNode); 151 152 $data['renderer']->addToNodestack($node); 153 } 154 155 public function handleParseUnknown(Doku_Event $event) 156 { 157 if (($event->data['node']['type'] ?? '') !== 'catmenu') return; 158 159 $event->data['newNode'] = new CatmenuNode($event->data['node'], $event->data['parent']); 160 $event->preventDefault(); 161 $event->stopPropagation(); 162 } 163 164 public function handlePagesiconUpdated(Doku_Event $event): void 165 { 166 $this->invalidateCacheForTarget('{{catmenu>'); 167 } 168} 169