xref: /plugin/dev/Skeletor.php (revision 70316b849e063a6d74c051966388525c6a62b604)
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', explode(' ', $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');
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 = '')
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        $replacements = $this->actionReplacements('EVENT_NAME'); // FIXME accept multiple optional events
139        $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class;
140        $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self;
141        $this->loadSkeleton($type . '.php', $path, $replacements);
142    }
143
144    /**
145     * Add test framework optionally with a specific test
146     *
147     * @param string $test Name of the Test to add
148     */
149    public function addTest($test = '')
150    {
151        $test = ucfirst(strtolower($test));
152        $this->loadSkeleton('.github/workflows/phpTestLinux.yml');
153        if ($test) {
154            $replacements = ['@@TEST@@' => $test];
155            $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements);
156        } else {
157            $this->loadSkeleton('_test/GeneralTest.php');
158        }
159    }
160
161    /**
162     * Add configuration
163     *
164     * @param bool $translate if true the settings language file will be be added, too
165     */
166    public function addConf($translate = false)
167    {
168        $this->loadSkeleton('conf/default.php');
169        $this->loadSkeleton('conf/metadata.php');
170
171        if ($translate) {
172            $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php');
173        }
174    }
175
176    /**
177     * Add language
178     *
179     * Currently only english is added, theoretically this could also copy over the keys from an
180     * existing english language file.
181     *
182     * @param bool $conf if true the settings language file will be be added, too
183     */
184    public function addLang($conf = false)
185    {
186        $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php');
187        if ($conf) {
188            $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php');
189        }
190    }
191
192    // endregion
193
194
195    /**
196     * Prepare the string replacements
197     *
198     * @param array $replacements override defaults
199     * @return array
200     */
201    protected function prepareReplacements($replacements = [])
202    {
203        // defaults
204        $data = [
205            '@@AUTHOR_NAME@@' => $this->author,
206            '@@AUTHOR_MAIL@@' => $this->email,
207            '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@
208            '@@PLUGIN_DESC@@' => $this->desc,
209            '@@PLUGIN_URL@@' => $this->url,
210            '@@PLUGIN_TYPE@@' => $this->type,
211            '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl',
212            '@@DATE@@' => date('Y-m-d'),
213        ];
214
215        // merge given overrides
216        return array_merge($data, $replacements);
217    }
218
219    /**
220     * Replacements needed for action components.
221     *
222     * @param string $event FIXME support multiple events
223     * @return string[]
224     */
225    protected function actionReplacements($event)
226    {
227        $event = strtoupper($event);
228        $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_'));
229        $register = '        $controller->register_hook(\'' . $event . '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');';
230        $handler = '    public function ' . $fn . '(Doku_Event $event, $param)' . "\n"
231            . "    {\n"
232            . "    }\n";
233
234        return [
235            '@@REGISTER@@' => $register . "\n   ",
236            '@@HANDLERS@@' => $handler,
237        ];
238    }
239
240    /**
241     * Load a skeleton file, do the replacements and add it to the list of files
242     *
243     * @param string $skel Skeleton relative to the skel dir
244     * @param string $target File name in the final plugin/template, empty for same as skeleton
245     * @param array $replacements Non-default replacements to use
246     */
247    protected function loadSkeleton($skel, $target = '', $replacements = [])
248    {
249        $replacements = $this->prepareReplacements($replacements);
250        if (!$target) $target = $skel;
251
252
253        $file = __DIR__ . '/skel/' . $skel;
254        if (!file_exists($file)) {
255            throw new RuntimeException('Skeleton file not found: ' . $skel);
256        }
257        $content = file_get_contents($file);
258        $this->files[$target] = str_replace(
259            array_keys($replacements),
260            array_values($replacements),
261            $content
262        );
263    }
264}
265