Scratchpads V1

This commit is contained in:
2026-01-09 09:26:39 +01:00
parent 16a07701ee
commit 0948f50d76
15 changed files with 718 additions and 27 deletions

183
syntax/scratchpad.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\Path;
use dokuwiki\plugin\luxtools\ScratchpadMap;
/**
* luxtools Plugin: Scratchpad syntax.
*
* Renders the contents of a configured file as wikitext and provides a minimal
* inline editor that saves directly to that file (no page revisions).
*/
class syntax_plugin_luxtools_scratchpad extends SyntaxPlugin
{
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
// After most formatting, similar to other luxtools syntaxes
return 223;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('\{\{scratchpad>.+?\}\}', $mode, 'plugin_luxtools_scratchpad');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// Do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$match = substr($match, strlen('{{scratchpad>'), -2);
[$path,] = array_pad(explode('&', $match, 2), 2, '');
return [
'path' => trim((string)$path),
];
}
/** @inheritdoc */
public function render($format, Doku_Renderer $renderer, $data)
{
if ($data === false) return false;
if (!is_array($data)) return false;
if ($format !== 'xhtml' && $format !== 'odt') return false;
// Always disable caching: the scratchpad is external to page revisions.
$renderer->nocache();
$rawPad = (string)($data['path'] ?? '');
if ($rawPad === '') {
$this->renderError($renderer, 'scratchpad_err_nopath');
return true;
}
$pathInfo = $this->getScratchpadPathInfoSafe($rawPad, $renderer);
if ($pathInfo === false) return true;
$filePath = (string)($pathInfo['path'] ?? '');
if ($filePath === '' || str_ends_with($filePath, '/')) {
$this->renderError($renderer, 'scratchpad_err_badpath');
return true;
}
// Never allow writing/reading within DokuWiki-controlled paths
if (Path::isWikiControlled(Path::cleanPath($filePath, false))) {
$this->renderError($renderer, 'error_outsidejail');
return true;
}
$text = '';
if (@is_file($filePath) && @is_readable($filePath)) {
$text = (string)io_readFile($filePath, false);
}
if ($format === 'odt') {
$renderer->cdata($text);
return true;
}
/** @var Doku_Renderer_xhtml $renderer */
$endpoint = DOKU_BASE . 'lib/plugins/luxtools/scratchpad.php';
global $ID;
$pageId = (string)$ID;
$canEdit = function_exists('auth_quickaclcheck') ? (auth_quickaclcheck($pageId) >= AUTH_EDIT) : false;
$renderer->doc .= '<div class="luxtools-plugin luxtools-scratchpad"'
. ' data-luxtools-scratchpad="1"'
. ' data-endpoint="' . hsc($endpoint) . '"'
. ' data-pad="' . hsc($rawPad) . '"'
. ' data-pageid="' . hsc($pageId) . '"'
. '>';
$renderer->doc .= '<div class="luxtools-scratchpad-bar">';
if ($canEdit) {
$label = (string)$this->getLang('scratchpad_edit');
if ($label === '') $label = 'Edit';
$renderer->doc .= '<a href="#" class="luxtools-scratchpad-edit" title="' . hsc($label) . '" aria-label="' . hsc($label) . '">✎</a>';
}
$renderer->doc .= '</div>';
$renderer->doc .= '<div class="luxtools-scratchpad-view">';
$renderer->doc .= $this->renderWikitextFragment($text);
$renderer->doc .= '</div>';
if ($canEdit) {
$renderer->doc .= '<div class="luxtools-scratchpad-editor" hidden>';
$renderer->doc .= '<textarea class="luxtools-scratchpad-text" rows="10" spellcheck="true"></textarea>';
$renderer->doc .= '<div class="luxtools-scratchpad-actions">'
. '<button type="button" class="button luxtools-scratchpad-save">' . hsc((string)$this->getLang('scratchpad_save') ?: 'Save') . '</button>'
. '<button type="button" class="button luxtools-scratchpad-cancel">' . hsc((string)$this->getLang('scratchpad_cancel') ?: 'Cancel') . '</button>'
. '<span class="luxtools-scratchpad-status" aria-live="polite"></span>'
. '</div>';
$renderer->doc .= '</div>';
}
$renderer->doc .= '</div>';
return true;
}
protected function renderWikitextFragment(string $text): string
{
// Render wikitext to XHTML and return it as a string
$info = ['cache' => false];
$instructions = p_get_instructions($text);
return (string)p_render('xhtml', $instructions, $info);
}
protected function renderError(Doku_Renderer $renderer, string $langKey): void
{
$msg = (string)$this->getLang($langKey);
if ($msg === '') $msg = $langKey;
if ($renderer instanceof Doku_Renderer_xhtml) {
$renderer->doc .= '<div class="luxtools-plugin luxtools-scratchpad"><div class="luxtools-scratchpad-error">'
. hsc($msg)
. '</div></div>';
return;
}
$renderer->cdata('[n/a: ' . $msg . ']');
}
/**
* Resolve a scratchpad alias to its file path using the configured scratchpad_paths setting.
*
* @param string $pad
* @param Doku_Renderer $renderer
* @return array|false
*/
protected function getScratchpadPathInfoSafe(string $pad, Doku_Renderer $renderer)
{
try {
$map = new ScratchpadMap((string)$this->getConf('scratchpad_paths'));
return [
'path' => $map->resolve($pad),
];
} catch (Exception $e) {
$this->renderError($renderer, 'scratchpad_err_unknown');
return false;
}
}
}