1<?php 2 3namespace dokuwiki\plugin\dev; 4 5use RuntimeException; 6 7/** 8 * This class holds basic information about a plugin or template and uses the skeleton files to 9 * create new plugin or template specific versions of them. 10 * 11 * This class does not write any files, but only provides the data for the actual file creation. 12 */ 13class Skeletor 14{ 15 // FIXME this may change upstream we may want to update it via github action 16 const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli']; 17 18 const TYPE_PLUGIN = 'plugin'; 19 const TYPE_TEMPLATE = 'template'; 20 21 protected $type; 22 protected $base; 23 protected $author; 24 protected $desc; 25 protected $name; 26 protected $email; 27 protected $url; 28 protected $dir; 29 30 /** @var array The files to be created in the form of [path => content] */ 31 protected $files = []; 32 33 /** 34 * Initialize the skeletor from provided data 35 * 36 * @param string $type 37 * @param string $base 38 * @param string $desc 39 * @param string $author 40 * @param string $email 41 * @param string $name 42 * @param string $url 43 */ 44 public function __construct($type, $base, $desc, $author, $email, $name = '', $url = '') 45 { 46 $this->type = $type; 47 $this->base = $base; 48 $this->desc = $desc; 49 $this->author = $author; 50 $this->email = $email; 51 $this->name = $name ?: ucfirst($base . ' ' . $type); 52 53 if ($type == self::TYPE_PLUGIN) { 54 $this->url = $url ?: 'https://www.dokuwiki.org/plugin:' . $base; 55 $this->dir = 'lib/plugins/' . $base; 56 } else { 57 $this->url = $url ?: 'https://www.dokuwiki.org/template:' . $base; 58 $this->dir = 'lib/tpl/' . $base; 59 } 60 } 61 62 /** 63 * Create an instance using an existing plugin or template directory 64 * 65 * @param string $dir 66 * @return Skeletor 67 */ 68 static public function fromDir($dir) 69 { 70 if (file_exists($dir . '/plugin.info.txt')) { 71 $type = self::TYPE_PLUGIN; 72 } elseif (file_exists($dir . '/template.info.txt')) { 73 $type = self::TYPE_TEMPLATE; 74 } else { 75 throw new RuntimeException('Not a plugin or template directory'); 76 } 77 78 $data = file($dir . '/' . $type . '.info.txt', FILE_IGNORE_NEW_LINES); 79 $data = array_map(function ($item) { 80 return array_map('trim', sexplode(' ', $item, 2, '')); 81 }, $data); 82 $data = array_combine(array_column($data, 0), array_column($data, 1)); 83 84 return new self($type, $data['base'], $data['desc'], $data['author'], $data['email'], $data['url']); 85 } 86 87 /** 88 * Return the files to be created 89 * 90 * @return array [path => content] 91 */ 92 public function getFiles() 93 { 94 return $this->files; 95 } 96 97 // region content creators 98 99 /** 100 * Add the basic files to the plugin 101 */ 102 public function addBasics() 103 { 104 $this->loadSkeleton('info.txt', $this->type . '.info.txt'); 105 $this->loadSkeleton('README'); 106 $this->loadSkeleton('LICENSE'); 107 $this->loadSkeleton('_gitattributes', '.gitattributes'); 108 } 109 110 /** 111 * Add another component to the plugin 112 * 113 * @param string $type 114 * @param string $component 115 */ 116 public function addComponent($type, $component = '', $options = []) 117 { 118 if ($this->type !== self::TYPE_PLUGIN) { 119 throw new RuntimeException('Components can only be added to plugins'); 120 } 121 122 if (!in_array($type, self::PLUGIN_TYPES)) { 123 throw new RuntimeException('Invalid type ' . $type); 124 } 125 126 $plugin = $this->base; 127 128 if ($component) { 129 $path = $type . '/' . $component . '.php'; 130 $class = $type . '_plugin_' . $plugin . '_' . $component; 131 $self = 'plugin_' . $plugin . '_' . $component; 132 } else { 133 $path = $type . '.php'; 134 $class = $type . '_plugin_' . $plugin; 135 $self = 'plugin_' . $plugin; 136 } 137 138 if ($type === 'action') { 139 $replacements = $this->actionReplacements($options); 140 } 141 if ($type === 'renderer' && isset($options[0]) && $options[0] === 'Doku_Renderer_xhtml') { 142 $type = 'renderer_xhtml'; // different template then 143 } 144 145 $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class; 146 $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self; 147 $this->loadSkeleton($type . '.php', $path, $replacements); 148 } 149 150 /** 151 * Add test framework optionally with a specific test 152 * 153 * @param string $test Name of the Test to add 154 */ 155 public function addTest($test = '') 156 { 157 // pick a random day and time for the cron job 158 $cron = sprintf( 159 '%d %d %d * *', 160 random_int(0, 59), 161 random_int(0, 23), 162 random_int(1, 28) 163 ); 164 165 $test = ucfirst($test); 166 $this->loadSkeleton('.github/workflows/dokuwiki.yml', '', ['@@CRON@@' => $cron]); 167 if ($test) { 168 $replacements = ['@@TEST@@' => $test]; 169 $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements); 170 } else { 171 $this->loadSkeleton('_test/GeneralTest.php'); 172 } 173 } 174 175 /** 176 * Add configuration 177 * 178 * @param bool $translate if true the settings language file will be be added, too 179 */ 180 public function addConf($translate = false) 181 { 182 $this->loadSkeleton('conf/default.php'); 183 $this->loadSkeleton('conf/metadata.php'); 184 185 if ($translate) { 186 $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 187 } 188 } 189 190 /** 191 * Add language 192 * 193 * Currently only english is added, theoretically this could also copy over the keys from an 194 * existing english language file. 195 * 196 * @param bool $conf if true the settings language file will be be added, too 197 */ 198 public function addLang($conf = false) 199 { 200 $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php'); 201 if ($conf) { 202 $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 203 } 204 } 205 206 /** 207 * Add the AGENTS.md file 208 * 209 * @return void 210 */ 211 public function addAgents() 212 { 213 $this->loadSkeleton('AGENTS.md'); 214 } 215 216 // endregion 217 218 219 /** 220 * Prepare the string replacements 221 * 222 * @param array $replacements override defaults 223 * @return array 224 */ 225 protected function prepareReplacements($replacements = []) 226 { 227 // defaults 228 $data = [ 229 '@@AUTHOR_NAME@@' => $this->author, 230 '@@AUTHOR_MAIL@@' => $this->email, 231 '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@ 232 '@@PLUGIN_DESC@@' => $this->desc, 233 '@@PLUGIN_URL@@' => $this->url, 234 '@@PLUGIN_TYPE@@' => $this->type, 235 '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl', 236 '@@DATE@@' => date('Y-m-d'), 237 ]; 238 239 // merge given overrides 240 return array_merge($data, $replacements); 241 } 242 243 /** 244 * Replacements needed for action components. 245 * 246 * @param string[] $event Event names to handle 247 * @return string[] 248 */ 249 protected function actionReplacements($events = []) 250 { 251 if (!$events) $events = ['EXAMPLE_EVENT']; 252 253 $register = ''; 254 $handler = ''; 255 256 $template = file_get_contents(__DIR__ . '/skel/action_handler.php'); 257 258 foreach ($events as $event) { 259 $event = strtoupper($event); 260 $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_')); 261 262 $register .= ' $controller->register_hook(\'' . $event . 263 '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');' . "\n"; 264 265 $handler .= str_replace(['@@EVENT@@', '@@HANDLER@@'], [$event, $fn], $template); 266 } 267 268 return [ 269 '@@REGISTER@@' => rtrim($register, "\n"), 270 '@@HANDLERS@@' => rtrim($handler, "\n"), 271 ]; 272 } 273 274 /** 275 * Load a skeleton file, do the replacements and add it to the list of files 276 * 277 * @param string $skel Skeleton relative to the skel dir 278 * @param string $target File name in the final plugin/template, empty for same as skeleton 279 * @param array $replacements Non-default replacements to use 280 */ 281 protected function loadSkeleton($skel, $target = '', $replacements = []) 282 { 283 $replacements = $this->prepareReplacements($replacements); 284 if (!$target) $target = $skel; 285 286 287 $file = __DIR__ . '/skel/' . $skel; 288 if (!file_exists($file)) { 289 throw new RuntimeException('Skeleton file not found: ' . $skel); 290 } 291 $content = file_get_contents($file); 292 $this->files[$target] = str_replace( 293 array_keys($replacements), 294 array_values($replacements), 295 $content 296 ); 297 } 298} 299