first version of the plugin wizard interface is working

This commit is contained in:
Andreas Gohr
2023-04-07 11:14:19 +02:00
parent 70316b849e
commit 89e2f9d127
14 changed files with 748 additions and 18 deletions

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
/.* export-ignore
/_test export-ignore
/composer.* export-ignore
/www export-ignore

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

View File

@ -113,7 +113,7 @@ class Skeletor
* @param string $type
* @param string $component
*/
public function addComponent($type, $component = '')
public function addComponent($type, $component = '', $options = [])
{
if ($this->type !== self::TYPE_PLUGIN) {
throw new RuntimeException('Components can only be added to plugins');
@ -135,7 +135,10 @@ class Skeletor
$self = 'plugin_' . $plugin;
}
$replacements = $this->actionReplacements('EVENT_NAME'); // FIXME accept multiple optional events
if($type === 'action') {
$replacements = $this->actionReplacements($options);
}
$replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class;
$replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self;
$this->loadSkeleton($type . '.php', $path, $replacements);
@ -219,20 +222,30 @@ class Skeletor
/**
* Replacements needed for action components.
*
* @param string $event FIXME support multiple events
* @param string[] $event Event names to handle
* @return string[]
*/
protected function actionReplacements($event)
protected function actionReplacements($events = [])
{
$event = strtoupper($event);
$fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_'));
$register = ' $controller->register_hook(\'' . $event . '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');';
$handler = ' public function ' . $fn . '(Doku_Event $event, $param)' . "\n"
. " {\n"
. " }\n";
if (!$events) $events = ['EXAMPLE_EVENT'];
$register = '';
$handler = '';
$template = file_get_contents(__DIR__ . '/skel/action_handler.php');
foreach ($events as $event) {
$event = strtoupper($event);
$fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_'));
$register .= ' $controller->register_hook(\'' . $event .
'\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');'. "\n";
$handler .= str_replace(['@@EVENT@@','@@HANDLER@@'], [$event, $fn], $template);
}
return [
'@@REGISTER@@' => $register . "\n ",
'@@REGISTER@@' => $register,
'@@HANDLERS@@' => $handler,
];
}

20
composer.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "dokuwiki/plugin-dev",
"description": "DokuWiki dev plugin and wizard",
"type": "project",
"license": "GPLv2",
"autoload": {
"psr-4": {
"dokuwiki\\plugin\\dev\\": "./"
}
},
"authors": [
{
"name": "Andreas Gohr",
"email": "andi@splitbrain.org"
}
],
"require": {
"splitbrain/php-archive": "^1.3"
}
}

79
composer.lock generated Normal file
View File

@ -0,0 +1,79 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "40bd143cf974e1a5ab22aa835987ef41",
"packages": [
{
"name": "splitbrain/php-archive",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/splitbrain/php-archive.git",
"reference": "d274e5190ba309777926348900cf9578d9e533c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/splitbrain/php-archive/zipball/d274e5190ba309777926348900cf9578d9e533c9",
"reference": "d274e5190ba309777926348900cf9578d9e533c9",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"ext-bz2": "*",
"ext-zip": "*",
"mikey179/vfsstream": "^1.6",
"phpunit/phpunit": "^8"
},
"suggest": {
"ext-bz2": "For bz2 compression",
"ext-iconv": "Used for proper filename encode handling",
"ext-mbstring": "Can be used alternatively for handling filename encoding",
"ext-zlib": "For zlib compression"
},
"type": "library",
"autoload": {
"psr-4": {
"splitbrain\\PHPArchive\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andreas Gohr",
"email": "andi@splitbrain.org"
}
],
"description": "Pure-PHP implementation to read and write TAR and ZIP archives",
"keywords": [
"archive",
"extract",
"tar",
"unpack",
"unzip",
"zip"
],
"support": {
"issues": "https://github.com/splitbrain/php-archive/issues",
"source": "https://github.com/splitbrain/php-archive/tree/1.3.1"
},
"time": "2022-03-23T09:21:55+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

93
events.txt Normal file
View File

@ -0,0 +1,93 @@
ACTION_ACT_PREPROCESS
ACTION_EXPORT_POSTPROCESS
ACTION_HANDLE_SUBSCRIBE
ACTION_HEADERS_SEND
ACTION_SHOW_REDIRECT
AJAX_CALL_UNKNOWN
AUTH_ACL_CHECK
AUTH_LOGIN_CHECK
AUTH_PASSWORD_GENERATE
AUTH_USER_CHANGE
COMMON_NOTIFY_ADDRESSLIST
COMMON_PAGETPL_LOAD
COMMON_PAGE_FROMTEMPLATE
COMMON_USER_LINK
COMMON_WIKIPAGE_SAVE
COMMON_WORDBLOCK_BLOCKED
CONFUTIL_CDN_SELECT
CSS_CACHE_USE
CSS_STYLES_INCLUDED
DETAIL_STARTED
DOKUWIKI_DONE
DOKUWIKI_STARTED
FEED_DATA_PROCESS
FEED_ITEM_ADD
FEED_MODE_UNKNOWN
FEED_OPTS_POSTPROCESS
FETCH_MEDIA_STATUS
FORM_QUICKSEARCH_OUTPUT
FORM_SEARCH_OUTPUT
FULLTEXT_SNIPPET_CREATE
HTML_CONFLICTFORM_OUTPUT
HTML_DRAFTFORM_OUTPUT
HTML_EDITFORM_OUTPUT
HTML_EDIT_FORMSELECTION
HTML_LOGINFORM_OUTPUT
HTML_PAGE_FROMTEMPLATE
HTML_RECENTFORM_OUTPUT
HTML_REGISTERFORM_OUTPUT
HTML_RESENDPWDFORM_OUTPUT
HTML_REVISIONSFORM_OUTPUT
HTML_SECEDIT_BUTTON
HTML_SHOWREV_OUTPUT
HTML_SUBSCRIBEFORM_OUTPUT
HTML_UPDATEPROFILEFORM_OUTPUT
HTML_UPLOADFORM_OUTPUT
HTTPCLIENT_REQUEST_SEND
INDEXER_PAGE_ADD
INDEXER_TASKS_RUN
INDEXER_TEXT_PREPARE
INDEXER_VERSION_GET
INIT_LANG_LOAD
IO_NAMESPACE_CREATED
IO_NAMESPACE_DELETED
IO_WIKIPAGE_READ
IO_WIKIPAGE_WRITE
JS_CACHE_USE
JS_SCRIPT_LIST
MAIL_MESSAGE_SEND
MANIFEST_SEND
MEDIAMANAGER_CONTENT_OUTPUT
MEDIAMANAGER_STARTED
MEDIA_DELETE_FILE
MEDIA_SENDFILE
MEDIA_UPLOAD_FINISH
MENU_ITEMS_ASSEMBLY
PAGEUTILS_ID_HIDEPAGE
PARSER_CACHE_USE
PARSER_HANDLER_DONE
PARSER_METADATA_RENDER
PARSER_WIKITEXT_PREPROCESS
PLUGIN_CONFIG_PLUGINLIST
PLUGIN_PLUGINMANAGER_PLUGINLIST
PLUGIN_POPULARITY_DATA_SETUP
RENDERER_CONTENT_POSTPROCESS
RPC_CALL_ADD
SEARCH_QUERY_FULLPAGE
SEARCH_QUERY_PAGELOOKUP
SEARCH_RESULT_FULLPAGE
SEARCH_RESULT_PAGELOOKUP
SITEMAP_GENERATE
SITEMAP_PING
TEMPLATE_PAGETOOLS_DISPLAY
TEMPLATE_SITETOOLS_DISPLAY
TEMPLATE_USERTOOLS_DISPLAY
TOOLBAR_DEFINE
TPL_ACTION_GET
TPL_ACT_RENDER
TPL_ACT_UNKNOWN
TPL_CONTENT_DISPLAY
TPL_IMG_DISPLAY
TPL_METAHEADER_OUTPUT
TPL_TOC_RENDER
XMLRPC_CALLBACK_REGISTER

View File

@ -14,13 +14,6 @@ class @@PLUGIN_COMPONENT_NAME@@ extends \dokuwiki\Extension\ActionPlugin
@@REGISTER@@
}
/**
* FIXME Event handler for
*
* @param Doku_Event $event event object by reference
* @param mixed $param optional parameter passed when event was registered
* @return void
*/
@@HANDLERS@@
}

12
skel/action_handler.php Normal file
View File

@ -0,0 +1,12 @@
/**
* Event handler for @@EVENT@@
*
* @see https://www.dokuwiki.org/devel:events:@@EVENT@@
* @param Doku_Event $event Event object
* @param mixed $param optional parameter passed when event was registered
* @return void
*/
public function @@HANDLER@@(Doku_Event $event, $param) {
}

76
www/PluginWizard.php Normal file
View File

@ -0,0 +1,76 @@
<?php
namespace dokuwiki\plugin\dev\www;
use dokuwiki\plugin\dev\Skeletor;
use splitbrain\PHPArchive\ArchiveIllegalCompressionException;
use splitbrain\PHPArchive\ArchiveIOException;
use splitbrain\PHPArchive\Zip;
class PluginWizard
{
/**
* @throws ArchiveIllegalCompressionException
* @throws ArchiveIOException
*/
public function handle()
{
if (!isset($_POST['base'])) return null;
$skeletor = new Skeletor(
Skeletor::TYPE_PLUGIN,
$_POST['base'] ?: '', //FIXME clean base
$_POST['desc'] ?: '',
$_POST['author'] ?: '',
$_POST['mail'] ?: '',
'',
$_POST['url'] ?: ''
);
$skeletor->addBasics();
if (!empty($_POST['use_lang'])) $skeletor->addLang();
if (!empty($_POST['use_conf'])) $skeletor->addConf();
if (!empty($_POST['use_test'])) $skeletor->addTest();
foreach ($_POST['components'] as $id) {
list($type, /*"plugin"*/, /*base*/, $component) = array_pad(explode('_', $id, 4), 4, '');
if (isset($_POST['options'][$id])) {
$options = array_filter(array_map('trim', explode(',', $_POST['options'][$id])));
} else {
$options = [];
}
$skeletor->addComponent($type, $component, $options);
}
$zip = new Zip();
$zip->setCompression(9);
$zip->create();
foreach ($skeletor->getFiles() as $file => $content) {
$zip->addData($_POST['base'] . '/' . $file, $content);
}
return $zip->getArchive();
}
/**
* Get options for all available plugin types
*/
public function getPluginTypes()
{
return Skeletor::PLUGIN_TYPES;
}
public function getEvents()
{
return array_map('trim', file(__DIR__ . '/../events.txt', FILE_IGNORE_NEW_LINES));
}
}

104
www/awesomplete.css Normal file
View File

@ -0,0 +1,104 @@
.awesomplete [hidden] {
display: none;
}
.awesomplete .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0);
}
.awesomplete {
display: inline-block;
position: relative;
}
.awesomplete > input {
display: block;
}
.awesomplete > ul {
position: absolute;
left: 0;
z-index: 1;
min-width: 100%;
box-sizing: border-box;
list-style: none;
padding: 0;
margin: 0;
background: #fff;
}
.awesomplete > ul:empty {
display: none;
}
.awesomplete > ul {
border-radius: .3em;
margin: .2em 0 0;
background: hsla(0,0%,100%,.9);
background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
border: 1px solid rgba(0,0,0,.3);
box-shadow: .05em .2em .6em rgba(0,0,0,.2);
text-shadow: none;
}
@supports (transform: scale(0)) {
.awesomplete > ul {
transition: .3s cubic-bezier(.4,.2,.5,1.4);
transform-origin: 1.43em -.43em;
}
.awesomplete > ul[hidden],
.awesomplete > ul:empty {
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease;
}
}
/* Pointer */
.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0; height: 0;
padding: .4em;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.awesomplete > ul > li {
position: relative;
padding: .2em .5em;
cursor: pointer;
}
.awesomplete > ul > li:hover {
background: hsl(200, 40%, 80%);
color: black;
}
.awesomplete > ul > li[aria-selected="true"] {
background: hsl(205, 40%, 40%);
color: white;
}
.awesomplete mark {
background: hsl(65, 100%, 50%);
}
.awesomplete li:hover mark {
background: hsl(68, 100%, 41%);
}
.awesomplete li[aria-selected="true"] mark {
background: hsl(86, 100%, 21%);
color: inherit;
}
/*# sourceMappingURL=awesomplete.css.map */

3
www/awesomplete.min.js vendored Normal file

File diff suppressed because one or more lines are too long

145
www/index.php Normal file
View File

@ -0,0 +1,145 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$WIZ = new dokuwiki\plugin\dev\www\PluginWizard();
try {
$archive = $WIZ->handle();
if($archive) {
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="plugin.zip"');
echo $archive;
exit;
}
} catch (Exception $e) {
// FIXME handle errors
}
header('Content-Type: text/html; charset=utf-8');
?>
<html lang="en">
<head>
<title>DokuWiki Plugin Wizard</title>
<script type="text/javascript">
const ACTION_EVENTS = <?php echo json_encode($WIZ->getEvents()); ?>;
</script>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="awesomplete.css" /
</head>
<body>
<main>
<h1>DokuWiki Plugin Wizard</h1>
<div class="intro">
<p>
This wizard generates a <a href="https://www.dokuwiki.org/devel:plugins">DokuWiki plugin</a>
skeleton to help you get started with coding your plugin.
Before using it you should familiarize your self with how plugins in DokuWiki work
and determine what components your plugin will need.
</p>
<p>
To use it, fill in the general plugin info and add plugin components. Once you're
done, click "create" and download your plugin skeleton.
</p>
<p>
Alternatively you can also use the <a href="https://www.dokuwiki.org/plugin:dev">dev plugin</a>.
This plugin will also come in handy when editing and extending your plugin later.
</p>
</div>
<noscript>
<div class="nojs">
Sorry, this wizard needs JavaScript to do its magic. It will not work with your
current setup.
</div>
</noscript>
<form action="index.php" method="post" id="ajax__plugin_wiz">
<section>
<div id="plugin_info">
<h2>Plugin Information</h2>
<label>
<span>Plugin base name:</span>
<input type="text" name="base" required="required" pattern="^[a-z0-9]+$"
placeholder="yourplugin">
<small>(lowercase, no special chars)</small>
</label>
<label>
<span>A short description of what the plugin does:</span>
<input type="text" name="desc" required="required"
placeholder="A plugin to flurb the blarg">
</label>
<label>
<span>Your name:</span>
<input type="text" name="author" required="required" placeholder="Jane Doe">
</label>
<label>
<span>Your E-Mail address:</span>
<input type="text" name="mail" required="required" placeholder="jane@example.com">
</label>
<label>
<span>URL for the plugin:</span>
<input type="text" name="url" placeholder="https://www.dokuwiki.org/plugin:yourplugin">
<small>(leave empty for default dokuwiki.org location)</small>
</label>
<label>
<input type="checkbox" name="use_lang" value="1"/>
<span>Use localization</span>
</label>
<label>
<input type="checkbox" name="use_conf" value="1"/>
<span>Use configuration</span>
</label>
<label>
<input type="checkbox" name="use_tests" value="1"/>
<span>Use unit tests</span>
</label>
</div>
<div id="plugin_components">
<h2>Add Plugin Components</h2>
<label>
<span>Type:</span>
<select>
<?php foreach ($WIZ->getPluginTypes() as $type): ?>
<option value="<?php echo $type ?>"><?php echo ucfirst($type) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Add as a Sub-Component:</span>
<input type="text" value="" pattern="^[a-z0-9]+$" placeholder="subcomponent"/>
<small>(leave empty to add top level)</small>
</label>
<button type="button">Add Component</button>
<ul id="output"></ul>
</div>
</section>
<button type="submit" name="plugin_wiz_create">Create and Download<br>Plugin Skeleton</button>
</form>
</main>
<script src="awesomplete.min.js"></script>
<script src="script.js"></script>
</body>
</html>

112
www/script.js Normal file
View File

@ -0,0 +1,112 @@
const plugin = document.getElementById('plugin_info');
const component = document.getElementById('plugin_components');
const output = document.getElementById('output');
updatePluginNameStatus();
/**
* disable the plugin name field when a component has been added
*/
function updatePluginNameStatus() {
plugin.querySelector('input').readOnly = output.children.length > 0;
document.querySelector('button[type=submit]').disabled = output.children.length === 0;
}
/**
* Create the HTML element for a component
*
* @param {string} plugin The plugin base name
* @param {string} type The component type
* @param {string} name The component name
* @returns {HTMLLIElement|null} null if the component already exists
*/
function createComponentElement(plugin, type, name) {
let id = `${type}_plugin_${plugin}`;
if (name !== '') {
id += `_${name}`;
}
if (document.getElementById(`component-${id}`)) {
return null;
}
const li = document.createElement('li');
li.id = `component-${id}`;
li.dataset.type = type;
li.dataset.name = name;
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'components[]';
hidden.value = id;
li.append(hidden);
const remove = document.createElement('button');
remove.type = 'button';
remove.innerText = 'X';
remove.title = 'Remove this component';
remove.addEventListener('click', function (event) {
li.remove();
updatePluginNameStatus();
});
li.append(remove);
const label = document.createElement('span');
label.innerText = id;
li.append(label);
// add auto completion for events
if (type === 'action') {
const events = document.createElement('input');
events.type = 'text';
events.name = `options[${id}]`;
events.placeholder = 'EVENTS_TO_HANDLE, ...';
li.append(events);
new Awesomplete(events, {
list: ACTION_EVENTS,
filter: function (text, input) {
return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]);
},
item: function (text, input) {
return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]);
},
replace: function (text) {
var before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value = before + text + ", ";
}
});
}
return li;
}
/**
* Add a component to the output list
*/
component.querySelector('button').addEventListener('click', function (event) {
const plugin_base = plugin.querySelector('input'); // first input field is plugin base name
const component_type = component.querySelector('select');
const component_name = component.querySelector('input');
if (!plugin_base.validity.valid) {
plugin_base.reportValidity();
plugin_base.focus();
return;
}
if (!component_name.validity.valid) {
component_name.reportValidity();
component_name.focus();
return;
}
const li = createComponentElement(plugin_base.value, component_type.value, component_name.value);
if (!li) return;
output.appendChild(li);
updatePluginNameStatus();
});

75
www/style.css Normal file
View File

@ -0,0 +1,75 @@
body, html {
font: 16px sans-serif;
color: #333;
}
main {
width: 100%;
max-width: 75em;
margin: 0 auto;
}
form section {
display: flex;
flex-wrap: wrap;
gap: 2em;
justify-content: space-between;
}
label {
display: block;
margin-bottom: 1em;
}
label > span:first-child {
display: block;
font-weight: bold;
}
label > select,
label > input[type="text"] {
width: 30em;
display: block;
font: 16px sans-serif;
}
label > input[type="checkbox"] + span {
font-weight: bold;
}
button, label {
cursor: pointer;
}
#output {
margin: 1em 0;
padding: 0;
list-style: none;
}
#output li {
margin: 1em 0;
}
#output li button {
float: right;
}
#output li span {
display: block;
}
#output li .awesomplete,
#output li input,
#output li select {
display: block;
width: 100%;
}
button[type="submit"] {
font-size: 120%;
width: 15em;
padding: 1em;
display: block;
margin: 1em auto;
}