mirror of
https://github.com/cosmocode/dokuwiki-plugin-prosemirror.git
synced 2025-07-22 01:28:32 +00:00
755 lines
18 KiB
PHP
755 lines
18 KiB
PHP
<?php
|
|
|
|
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
|
|
|
|
/**
|
|
* DokuWiki Plugin prosemirror (Renderer Component)
|
|
*
|
|
* @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
|
|
* @author Andreas Gohr <gohr@cosmocode.de>
|
|
*/
|
|
|
|
use dokuwiki\plugin\prosemirror\parser\ImageNode;
|
|
use dokuwiki\plugin\prosemirror\parser\LocalLinkNode;
|
|
use dokuwiki\plugin\prosemirror\parser\InternalLinkNode;
|
|
use dokuwiki\plugin\prosemirror\parser\ExternalLinkNode;
|
|
use dokuwiki\plugin\prosemirror\parser\InterwikiLinkNode;
|
|
use dokuwiki\plugin\prosemirror\parser\EmailLinkNode;
|
|
use dokuwiki\plugin\prosemirror\parser\WindowsShareLinkNode;
|
|
use dokuwiki\Extension\Event;
|
|
use dokuwiki\plugin\prosemirror\schema\Mark;
|
|
use dokuwiki\plugin\prosemirror\schema\Node;
|
|
use dokuwiki\plugin\prosemirror\schema\NodeStack;
|
|
|
|
class renderer_plugin_prosemirror extends Doku_Renderer
|
|
{
|
|
/** @var NodeStack */
|
|
public $nodestack;
|
|
|
|
/** @var NodeStack[] */
|
|
protected $nodestackBackup = [];
|
|
|
|
/** @var array list of currently active formatting marks */
|
|
protected $marks = [];
|
|
|
|
/** @var int column counter for table handling */
|
|
protected $colcount = 0;
|
|
|
|
/**
|
|
* The format this renderer produces
|
|
*/
|
|
public function getFormat()
|
|
{
|
|
return 'prosemirror';
|
|
}
|
|
|
|
public function addToNodestackTop(Node $node)
|
|
{
|
|
$this->nodestack->addTop($node);
|
|
}
|
|
|
|
public function addToNodestack(Node $node)
|
|
{
|
|
$this->nodestack->add($node);
|
|
}
|
|
|
|
public function dropFromNodeStack($nodeType)
|
|
{
|
|
$this->nodestack->drop($nodeType);
|
|
}
|
|
|
|
public function getCurrentMarks()
|
|
{
|
|
return $this->marks;
|
|
}
|
|
|
|
/**
|
|
* If there is a block scope open, close it.
|
|
*/
|
|
protected function clearBlock()
|
|
{
|
|
$parentNode = $this->nodestack->current()->getType();
|
|
if ($parentNode == 'paragraph') {
|
|
$this->nodestack->drop($parentNode);
|
|
}
|
|
}
|
|
|
|
// FIXME implement all methods of Doku_Renderer here
|
|
|
|
/** @inheritDoc */
|
|
public function document_start()
|
|
{
|
|
$this->nodestack = new NodeStack();
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function document_end()
|
|
{
|
|
if ($this->nodestack->isEmpty()) {
|
|
$this->p_open();
|
|
$this->p_close();
|
|
}
|
|
$this->doc = json_encode($this->nodestack->doc(), JSON_PRETTY_PRINT);
|
|
}
|
|
|
|
public function nocache()
|
|
{
|
|
$docNode = $this->nodestack->getDocNode();
|
|
$docNode->attr('nocache', true);
|
|
}
|
|
|
|
public function notoc()
|
|
{
|
|
$docNode = $this->nodestack->getDocNode();
|
|
$docNode->attr('notoc', true);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function p_open()
|
|
{
|
|
$this->nodestack->addTop(new Node('paragraph'));
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function p_close()
|
|
{
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function quote_open()
|
|
{
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
$this->nodestack->addTop(new Node('blockquote'));
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function quote_close()
|
|
{
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
$this->nodestack->drop('blockquote');
|
|
}
|
|
|
|
#region lists
|
|
|
|
/** @inheritDoc */
|
|
public function listu_open()
|
|
{
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
|
|
$this->nodestack->addTop(new Node('bullet_list'));
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function listu_close()
|
|
{
|
|
$this->nodestack->drop('bullet_list');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function listo_open()
|
|
{
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
|
|
$this->nodestack->addTop(new Node('ordered_list'));
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function listo_close()
|
|
{
|
|
$this->nodestack->drop('ordered_list');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function listitem_open($level, $node = false)
|
|
{
|
|
$this->nodestack->addTop(new Node('list_item'));
|
|
|
|
$paragraphNode = new Node('paragraph');
|
|
$this->nodestack->addTop($paragraphNode);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function listitem_close()
|
|
{
|
|
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
$this->nodestack->drop('list_item');
|
|
}
|
|
|
|
#endregion lists
|
|
|
|
#region table
|
|
|
|
/** @inheritDoc */
|
|
public function table_open($maxcols = null, $numrows = null, $pos = null)
|
|
{
|
|
$this->nodestack->addTop(new Node('table'));
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function table_close($pos = null)
|
|
{
|
|
$this->nodestack->drop('table');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function tablerow_open()
|
|
{
|
|
$this->nodestack->addTop(new Node('table_row'));
|
|
$this->colcount = 0;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function tablerow_close()
|
|
{
|
|
$node = $this->nodestack->drop('table_row');
|
|
$node->attr('columns', $this->colcount);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function tablecell_open($colspan = 1, $align = null, $rowspan = 1)
|
|
{
|
|
$this->openTableCell('table_cell', $colspan, $align, $rowspan);
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function tablecell_close()
|
|
{
|
|
$this->closeTableCell('table_cell');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function tableheader_open($colspan = 1, $align = null, $rowspan = 1)
|
|
{
|
|
$this->openTableCell('table_header', $colspan, $align, $rowspan);
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function tableheader_close()
|
|
{
|
|
$this->closeTableCell('table_header');
|
|
}
|
|
|
|
/**
|
|
* Add a new table cell to the top of the stack
|
|
*
|
|
* @param string $type either table_cell or table_header
|
|
* @param int $colspan
|
|
* @param string|null $align either null/left, center or right
|
|
* @param int $rowspan
|
|
*/
|
|
protected function openTableCell($type, $colspan, $align, $rowspan)
|
|
{
|
|
$this->colcount += $colspan;
|
|
|
|
$node = new Node($type);
|
|
$node->attr('colspan', $colspan);
|
|
$node->attr('rowspan', $rowspan);
|
|
$node->attr('align', $align);
|
|
|
|
$this->nodestack->addTop($node);
|
|
|
|
$node = new Node('paragraph');
|
|
$this->nodestack->addTop($node);
|
|
}
|
|
|
|
/**
|
|
* Remove a table cell from the top of the stack
|
|
*
|
|
* @param string $type either table_cell or table_header
|
|
*/
|
|
protected function closeTableCell($type)
|
|
{
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$this->nodestack->drop('paragraph');
|
|
}
|
|
|
|
$curNode = $this->nodestack->current();
|
|
$curNode->trimContentLeft();
|
|
$curNode->trimContentRight();
|
|
|
|
$this->nodestack->drop($type);
|
|
}
|
|
|
|
#endregion table
|
|
|
|
/** @inheritDoc */
|
|
public function header($text, $level, $pos)
|
|
{
|
|
$node = new Node('heading');
|
|
$node->attr('level', $level);
|
|
|
|
$tnode = new Node('text');
|
|
$tnode->setText($text);
|
|
|
|
$node->addChild($tnode);
|
|
|
|
$this->nodestack->add($node);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function cdata($text)
|
|
{
|
|
if ($text === '') {
|
|
return;
|
|
}
|
|
|
|
$parentNode = $this->nodestack->current()->getType();
|
|
|
|
if (in_array($parentNode, ['paragraph', 'footnote'])) {
|
|
$text = str_replace("\n", ' ', $text);
|
|
}
|
|
|
|
if ($parentNode === 'list_item') {
|
|
$node = new Node('paragraph');
|
|
$this->nodestack->addTop($node);
|
|
}
|
|
|
|
if ($parentNode === 'blockquote') {
|
|
$node = new Node('paragraph');
|
|
$this->nodestack->addTop($node);
|
|
}
|
|
|
|
if ($parentNode === 'doc') {
|
|
$node = new Node('paragraph');
|
|
$this->nodestack->addTop($node);
|
|
}
|
|
|
|
$node = new Node('text');
|
|
$node->setText($text);
|
|
foreach (array_keys($this->marks) as $mark) {
|
|
$node->addMark(new Mark($mark));
|
|
}
|
|
$this->nodestack->add($node);
|
|
}
|
|
|
|
public function preformatted($text)
|
|
{
|
|
$this->clearBlock();
|
|
$node = new Node('preformatted');
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata($text);
|
|
$this->nodestack->drop('preformatted');
|
|
}
|
|
|
|
public function code($text, $lang = null, $file = null)
|
|
{
|
|
$this->clearBlock();
|
|
$node = new Node('code_block');
|
|
$node->attr('class', 'code ' . $lang);
|
|
$node->attr('data-language', $lang);
|
|
$node->attr('data-filename', $file);
|
|
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata(trim($text, "\n"));
|
|
$this->nodestack->drop('code_block');
|
|
}
|
|
|
|
public function file($text, $lang = null, $file = null)
|
|
{
|
|
$this->code($text, $lang, $file);
|
|
}
|
|
|
|
public function html($text)
|
|
{
|
|
$node = new Node('html_inline');
|
|
$node->attr('class', 'html_inline');
|
|
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata(str_replace("\n", ' ', $text));
|
|
$this->nodestack->drop('html_inline');
|
|
}
|
|
|
|
public function htmlblock($text)
|
|
{
|
|
$this->clearBlock();
|
|
$node = new Node('html_block');
|
|
$node->attr('class', 'html_block');
|
|
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata(trim($text, "\n"));
|
|
$this->nodestack->drop('html_block');
|
|
}
|
|
|
|
public function php($text)
|
|
{
|
|
$node = new Node('php_inline');
|
|
$node->attr('class', 'php_inline');
|
|
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata(str_replace("\n", ' ', $text));
|
|
$this->nodestack->drop('php_inline');
|
|
}
|
|
|
|
public function phpblock($text)
|
|
{
|
|
$this->clearBlock();
|
|
$node = new Node('php_block');
|
|
$node->attr('class', 'php_block');
|
|
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata(trim($text, "\n"));
|
|
$this->nodestack->drop('php_block');
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function rss($url, $params)
|
|
{
|
|
$this->clearBlock();
|
|
$node = new Node('rss');
|
|
$node->attr('url', hsc($url));
|
|
$node->attr('max', $params['max']);
|
|
$node->attr('reverse', (bool)$params['reverse']);
|
|
$node->attr('author', (bool)$params['author']);
|
|
$node->attr('date', (bool)$params['date']);
|
|
$node->attr('details', (bool)$params['details']);
|
|
|
|
if ($params['refresh'] % 86400 === 0) {
|
|
$refresh = $params['refresh'] / 86400 . 'd';
|
|
} elseif ($params['refresh'] % 3600 === 0) {
|
|
$refresh = $params['refresh'] / 3600 . 'h';
|
|
} else {
|
|
$refresh = $params['refresh'] / 60 . 'm';
|
|
}
|
|
|
|
$node->attr('refresh', trim($refresh));
|
|
$this->nodestack->add($node);
|
|
}
|
|
|
|
|
|
public function footnote_open()
|
|
{
|
|
$footnoteNode = new Node('footnote');
|
|
$this->nodestack->addTop($footnoteNode);
|
|
$this->nodestackBackup[] = $this->nodestack;
|
|
$this->nodestack = new NodeStack();
|
|
}
|
|
|
|
public function footnote_close()
|
|
{
|
|
$json = json_encode($this->nodestack->doc());
|
|
$this->nodestack = array_pop($this->nodestackBackup);
|
|
$this->nodestack->current()->attr('contentJSON', $json);
|
|
$this->nodestack->drop('footnote');
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function internalmedia(
|
|
$src,
|
|
$title = null,
|
|
$align = null,
|
|
$width = null,
|
|
$height = null,
|
|
$cache = null,
|
|
$linking = null
|
|
) {
|
|
|
|
// FIXME how do we handle non-images, e.g. pdfs or audio?
|
|
ImageNode::render(
|
|
$this,
|
|
$src,
|
|
$title,
|
|
$align,
|
|
$width,
|
|
$height,
|
|
$cache,
|
|
$linking
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function externalmedia(
|
|
$src,
|
|
$title = null,
|
|
$align = null,
|
|
$width = null,
|
|
$height = null,
|
|
$cache = null,
|
|
$linking = null
|
|
) {
|
|
ImageNode::render(
|
|
$this,
|
|
$src,
|
|
$title,
|
|
$align,
|
|
$width,
|
|
$height,
|
|
$cache,
|
|
$linking
|
|
);
|
|
}
|
|
|
|
|
|
public function locallink($hash, $name = null)
|
|
{
|
|
LocalLinkNode::render($this, $hash, $name);
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function internallink($id, $name = null)
|
|
{
|
|
InternalLinkNode::render($this, $id, $name);
|
|
}
|
|
|
|
public function externallink($link, $title = null)
|
|
{
|
|
ExternalLinkNode::render($this, $link, $title);
|
|
}
|
|
|
|
public function interwikilink($link, $title, $wikiName, $wikiUri)
|
|
{
|
|
InterwikiLinkNode::render($this, $title, $wikiName, $wikiUri);
|
|
}
|
|
|
|
public function emaillink($address, $name = null)
|
|
{
|
|
EmailLinkNode::render($this, $address, $name);
|
|
}
|
|
|
|
public function windowssharelink($link, $title = null)
|
|
{
|
|
WindowsShareLinkNode::render($this, $link, $title);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function linebreak()
|
|
{
|
|
$this->nodestack->add(new Node('hard_break'));
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function hr()
|
|
{
|
|
$this->nodestack->add(new Node('horizontal_rule'));
|
|
}
|
|
|
|
public function plugin($name, $data, $state = '', $match = '')
|
|
{
|
|
if (empty($match)) {
|
|
return;
|
|
}
|
|
$eventData = [
|
|
'name' => $name,
|
|
'data' => $data,
|
|
'state' => $state,
|
|
'match' => $match,
|
|
'renderer' => $this,
|
|
];
|
|
$event = new Event('PROSEMIRROR_RENDER_PLUGIN', $eventData);
|
|
if ($event->advise_before()) {
|
|
if ($this->nodestack->current()->getType() === 'paragraph') {
|
|
$nodetype = 'dwplugin_inline';
|
|
} else {
|
|
$nodetype = 'dwplugin_block';
|
|
}
|
|
$node = new Node($nodetype);
|
|
$node->attr('class', 'dwplugin');
|
|
$node->attr('data-pluginname', $name);
|
|
$this->nodestack->addTop($node);
|
|
$this->cdata($match);
|
|
$this->nodestack->drop($nodetype);
|
|
}
|
|
}
|
|
|
|
public function smiley($smiley)
|
|
{
|
|
if (array_key_exists($smiley, $this->smileys)) {
|
|
$node = new Node('smiley');
|
|
$node->attr('icon', $this->smileys[$smiley]);
|
|
$node->attr('syntax', $smiley);
|
|
$this->nodestack->add($node);
|
|
} else {
|
|
$this->cdata($smiley);
|
|
}
|
|
}
|
|
|
|
#region elements with no special WYSIWYG representation
|
|
|
|
/** @inheritDoc */
|
|
public function entity($entity)
|
|
{
|
|
$this->cdata($entity); // FIXME should we handle them special?
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function multiplyentity($x, $y)
|
|
{
|
|
$this->cdata($x . 'x' . $y);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function acronym($acronym)
|
|
{
|
|
$this->cdata($acronym);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function apostrophe()
|
|
{
|
|
$this->cdata("'");
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function singlequoteopening()
|
|
{
|
|
$this->cdata("'");
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function singlequoteclosing()
|
|
{
|
|
$this->cdata("'");
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function doublequoteopening()
|
|
{
|
|
$this->cdata('"');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function doublequoteclosing()
|
|
{
|
|
$this->cdata('"');
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function camelcaselink($link)
|
|
{
|
|
$this->cdata($link); // FIXME should/could we decorate it?
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region formatter marks
|
|
|
|
/** @inheritDoc */
|
|
public function strong_open()
|
|
{
|
|
$this->marks['strong'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function strong_close()
|
|
{
|
|
if (isset($this->marks['strong'])) {
|
|
unset($this->marks['strong']);
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function emphasis_open()
|
|
{
|
|
$this->marks['em'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function emphasis_close()
|
|
{
|
|
if (isset($this->marks['em'])) {
|
|
unset($this->marks['em']);
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function subscript_open()
|
|
{
|
|
$this->marks['subscript'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function subscript_close()
|
|
{
|
|
if (isset($this->marks['subscript'])) {
|
|
unset($this->marks['subscript']);
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function superscript_open()
|
|
{
|
|
$this->marks['superscript'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function superscript_close()
|
|
{
|
|
if (isset($this->marks['superscript'])) {
|
|
unset($this->marks['superscript']);
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function monospace_open()
|
|
{
|
|
$this->marks['code'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function monospace_close()
|
|
{
|
|
if (isset($this->marks['code'])) {
|
|
unset($this->marks['code']);
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function deleted_open()
|
|
{
|
|
$this->marks['deleted'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function deleted_close()
|
|
{
|
|
if (isset($this->marks['deleted'])) {
|
|
unset($this->marks['deleted']);
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function underline_open()
|
|
{
|
|
$this->marks['underline'] = 1;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function underline_close()
|
|
{
|
|
if (isset($this->marks['underline'])) {
|
|
unset($this->marks['underline']);
|
|
}
|
|
}
|
|
|
|
|
|
/** @inheritDoc */
|
|
public function unformatted($text)
|
|
{
|
|
$this->marks['unformatted'] = 1;
|
|
parent::unformatted($text);
|
|
unset($this->marks['unformatted']);
|
|
}
|
|
|
|
|
|
#endregion formatter marks
|
|
}
|