xref: /plugin/dev/Skeletor.php (revision fe060d0d4c4c42403a614910f1d8cdcd8d360b96)
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    public const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli'];
17
18    public const TYPE_PLUGIN = 'plugin';
19    public 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    public static 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(fn($item) => array_map(trim(...), sexplode(' ', $item, 2, '')), $data);
80        $data = array_combine(array_column($data, 0), array_column($data, 1));
81
82        return new self($type, $data['base'], $data['desc'], $data['author'], $data['email'], $data['url']);
83    }
84
85    /**
86     * Return the files to be created
87     *
88     * @return array [path => content]
89     */
90    public function getFiles()
91    {
92        return $this->files;
93    }
94
95    // region content creators
96
97    /**
98     * Add the basic files to the plugin
99     */
100    public function addBasics()
101    {
102        $this->loadSkeleton('info.txt', $this->type . '.info.txt');
103        $this->loadSkeleton('README');
104        $this->loadSkeleton('LICENSE');
105        $this->loadSkeleton('_gitattributes', '.gitattributes');
106    }
107
108    /**
109     * Add another component to the plugin
110     *
111     * @param string $type
112     * @param string $component
113     */
114    public function addComponent($type, $component = '', $options = [])
115    {
116        if ($this->type !== self::TYPE_PLUGIN) {
117            throw new RuntimeException('Components can only be added to plugins');
118        }
119
120        if (!in_array($type, self::PLUGIN_TYPES)) {
121            throw new RuntimeException('Invalid type ' . $type);
122        }
123
124        $plugin = $this->base;
125
126        if ($component) {
127            $path = $type . '/' . $component . '.php';
128            $class = $type . '_plugin_' . $plugin . '_' . $component;
129            $self = 'plugin_' . $plugin . '_' . $component;
130        } else {
131            $path = $type . '.php';
132            $class = $type . '_plugin_' . $plugin;
133            $self = 'plugin_' . $plugin;
134        }
135
136        if ($type === 'action') {
137            $replacements = $this->actionReplacements($options);
138        }
139        if ($type === 'renderer' && isset($options[0]) && $options[0] === 'Doku_Renderer_xhtml') {
140            $type = 'renderer_xhtml'; // different template then
141        }
142
143        $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class;
144        $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self;
145        $this->loadSkeleton($type . '.php', $path, $replacements);
146    }
147
148    /**
149     * Add test framework optionally with a specific test
150     *
151     * @param string $test Name of the Test to add
152     */
153    public function addTest($test = '')
154    {
155        // pick a random day and time for the cron job
156        $cron = sprintf(
157            '%d %d %d * *',
158            random_int(0, 59),
159            random_int(0, 23),
160            random_int(1, 28)
161        );
162
163        $test = ucfirst($test);
164        $this->loadSkeleton('.github/workflows/dokuwiki.yml', '', ['@@CRON@@' => $cron]);
165        if ($test) {
166            $replacements = ['@@TEST@@' => $test];
167            $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements);
168        } else {
169            $this->loadSkeleton('_test/GeneralTest.php');
170        }
171    }
172
173    /**
174     * Add configuration
175     *
176     * @param bool $translate if true the settings language file will be be added, too
177     */
178    public function addConf($translate = false)
179    {
180        $this->loadSkeleton('conf/default.php');
181        $this->loadSkeleton('conf/metadata.php');
182
183        if ($translate) {
184            $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php');
185        }
186    }
187
188    /**
189     * Add language
190     *
191     * Currently only english is added, theoretically this could also copy over the keys from an
192     * existing english language file.
193     *
194     * @param bool $conf if true the settings language file will be be added, too
195     */
196    public function addLang($conf = false)
197    {
198        $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php');
199        if ($conf) {
200            $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php');
201        }
202    }
203
204    /**
205     * Add the AGENTS.md file
206     *
207     * @return void
208     */
209    public function addAgents()
210    {
211        $this->loadSkeleton('AGENTS.md');
212    }
213
214    // endregion
215
216
217    /**
218     * Prepare the string replacements
219     *
220     * @param array $replacements override defaults
221     * @return array
222     */
223    protected function prepareReplacements($replacements = [])
224    {
225        // defaults
226        $data = [
227            '@@AUTHOR_NAME@@' => $this->author,
228            '@@AUTHOR_MAIL@@' => $this->email,
229            '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@
230            '@@PLUGIN_DESC@@' => $this->desc,
231            '@@PLUGIN_URL@@' => $this->url,
232            '@@PLUGIN_TYPE@@' => $this->type,
233            '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl',
234            '@@DATE@@' => date('Y-m-d'),
235        ];
236
237        // merge given overrides
238        return array_merge($data, $replacements);
239    }
240
241    /**
242     * Replacements needed for action components.
243     *
244     * @param string[] $event Event names to handle
245     * @return string[]
246     */
247    protected function actionReplacements($events = [])
248    {
249        if (!$events) $events = ['EXAMPLE_EVENT'];
250
251        $register = '';
252        $handler = '';
253
254        $template = file_get_contents(__DIR__ . '/skel/action_handler.php');
255
256        foreach ($events as $event) {
257            $event = strtoupper($event);
258            $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_'));
259
260            $register .= '        $controller->register_hook(\'' . $event .
261                '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');' . "\n";
262
263            $handler .= str_replace(['@@EVENT@@', '@@HANDLER@@'], [$event, $fn], $template);
264        }
265
266        return [
267            '@@REGISTER@@' => rtrim($register, "\n"),
268            '@@HANDLERS@@' => rtrim($handler, "\n"),
269        ];
270    }
271
272    /**
273     * Load a skeleton file, do the replacements and add it to the list of files
274     *
275     * @param string $skel Skeleton relative to the skel dir
276     * @param string $target File name in the final plugin/template, empty for same as skeleton
277     * @param array $replacements Non-default replacements to use
278     */
279    protected function loadSkeleton($skel, $target = '', $replacements = [])
280    {
281        $replacements = $this->prepareReplacements($replacements);
282        if (!$target) $target = $skel;
283
284
285        $file = __DIR__ . '/skel/' . $skel;
286        if (!file_exists($file)) {
287            throw new RuntimeException('Skeleton file not found: ' . $skel);
288        }
289        $content = file_get_contents($file);
290        $this->files[$target] = str_replace(
291            array_keys($replacements),
292            array_values($replacements),
293            $content
294        );
295    }
296}
297