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