Commit 63bc84e4 authored by Jonny Bradley's avatar Jonny Bradley
Browse files

[ENH] Add Toast UI Editor for editing markdown pages

parent c7162ade
Pipeline #646799330 failed with stages
in 50 minutes and 10 seconds
......@@ -165,7 +165,8 @@ function ajax_preview(editorId, autoSaveId, inPage) {
var $textarea = $('#' + editorId);
var $autosavepreview = $("#autosave_preview");
syntaxHighlighter.sync($textarea);
const allowHtml = auto_save_allowHtml($textarea.prop("form"));
const allowHtml = auto_save_allowHtml($textarea.prop("form")),
is_markdown = $("input[name=syntax]").val() === "markdown" ? 1 : 0;
if (!ajaxPreviewWindow) {
if (inPage) {
var $prvw = $("#autosave_preview:visible");
......@@ -178,7 +179,8 @@ function ajax_preview(editorId, autoSaveId, inPage) {
autoSaveId: autoSaveId,
inPage: 1,
hdr: h,
allowHtml: allowHtml
allowHtml: allowHtml,
is_markdown: is_markdown
}, function(data) {
// remove JS and disarm links
data = data.replace(/\shref/gi, " tiki_href").
......@@ -190,14 +192,14 @@ function ajax_preview(editorId, autoSaveId, inPage) {
});
}
} else {
initPreviewWindow(editorId, autoSaveId, allowHtml);
initPreviewWindow(editorId, autoSaveId, allowHtml, is_markdown);
}
} else {
if (typeof ajaxPreviewWindow.get_new_preview === 'function') {
ajaxPreviewWindow.get_new_preview();
ajaxPreviewWindow.focus();
} else {
initPreviewWindow(editorId, autoSaveId, allowHtml);
initPreviewWindow(editorId, autoSaveId, allowHtml, is_markdown);
}
}
} else {
......@@ -208,9 +210,14 @@ function ajax_preview(editorId, autoSaveId, inPage) {
}
function initPreviewWindow (editorId, autoSaveId, allowHtml) {
function initPreviewWindow (editorId, autoSaveId, allowHtml, is_markdown) {
var features = 'menubar=no,toolbar=no,fullscreen=no,titlebar=no,status=no,width=600';
var url = $.service("edit", "preview", {editor_id: editorId, autoSaveId: autoSaveId, allowHtml: allowHtml });
var url = $.service("edit", "preview", {
editor_id: editorId,
autoSaveId: autoSaveId,
allowHtml: allowHtml,
is_markdown: is_markdown
});
window.open(url, "_blank", features);
}
......
......@@ -7,7 +7,7 @@
// $Id$
/*
* Shared functions for tiki implementation of nkeditor (v3.6.2)
* Shared functions for tiki implementation of ckeditor and toast ui for markdown
*/
use Tiki\Lib\core\Toolbar\ToolbarCombos;
......@@ -65,10 +65,10 @@ class WYSIWYGLib
$info = $tikilib->get_page_info($pageName, false); // Don't load page data.
$params = [
'_wysiwyg' => 'y',
'area_id' => 'page-data',
'comments' => '',
'is_html' => $info['is_html'], // temporary element id
'_wysiwyg' => 'y',
'area_id' => 'page-data',
'comments' => '',
'is_html' => $info['is_html'], // temporary element id
'switcheditor' => 'n',
];
......@@ -148,8 +148,9 @@ window.CKEDITOR.config.toolbar = ' . $cktools . ';
); // before dialog tools init (10)
}
if (
$auto_save_referrer && $prefs['feature_ajax'] === 'y' &&
$prefs['ajax_autosave'] === 'y' && $params['autosave'] == 'y'
$auto_save_referrer && $prefs['feature_ajax'] === 'y'
&& $prefs['ajax_autosave'] === 'y'
&& $params['autosave'] == 'y'
) {
$headerlib->add_js(
'// --- config settings for the autosave plugin ---
......@@ -206,6 +207,152 @@ ajaxLoadingShow("' . $dom_id . '");
return $ckoptions;
}
/**
* @param string $dom_id
* @param array $params
* @param string $auto_save_referrer
*
* @return array
*/
public function setUpMarkdownEditor(string $dom_id, string $content, array $params = [], string $auto_save_referrer = ''): array
{
global $prefs;
$matches = WikiParser_PluginMatcher::match($content);
$position = 0;
$newContent = '';
foreach ($matches as $match) {
$newContent .= substr($content, $position, $match->getStart() - $position);
$pluginMarkup = substr($content, $match->getStart(), $match->getEnd() - $match->getStart());
$mdCustomBlock = "\$\$tiki\n$pluginMarkup\n\$\$";
$newContent .= $mdCustomBlock;
$position = $match->getEnd();
}
$newContent .= substr($content, $position);
$content = $newContent;
/** @var HeaderLib $headerlib */
$headerlib = TikiLib::lib('header');
$options = [
'domId' => "$dom_id",
'height' => $prefs['markdown_wysiwyg_height'],
'previewStyle' => $prefs['markdown_wysiwyg_preview_style'],
'initialEditType' => $prefs['markdown_wysiwyg_intitial_edit_type'],
'usageStatistics' => $prefs['markdown_wysiwyg_usage_statistics'] === 'y',
'initialValue' => $content,
];
$languageCode = $this->languageMapISO($prefs['language']);
if ($languageCode) {
$options['language'] = $languageCode;
$headerlib->add_jsfile_external(
'https://uicdn.toast.com/editor/latest/i18n/' . strtolower($languageCode) . '.js'
);
}
if (! empty($params['_toolbars']) && $params['_toolbars'] === 'y') {
/** @var Smarty_Tiki $smarty */
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_function_toolbars');
$toolbarParams = [
'syntax' => 'markdown',
'area_id' => $dom_id,
'_wysiwyg' => 'y',
'is_html' => false,
];
$tuitools = smarty_function_toolbars($toolbarParams, $smarty->getEmptyInternalTemplate());
} else {
$tuitools = '[]';
}
$options['toolbarItems'] = $tuitools;
$jsonOptions = json_encode($options);
// using %~ at the start and end of values that need to be literals, like functions
$jsonOptions = preg_replace(['/"%~/', '/~%"/'], '', $jsonOptions);
$headerlib
//->add_jsfile('vendor_bundled/vendor/npm-asset/toast-ui--editor/dist/toastui-editor.js', true)
//->add_cssfile('vendor_bundled/vendor/npm-asset/toast-ui--editor/dist/toastui-editor.css')
//->add_cssfile('https://uicdn.toast.com/editor/latest/toastui-editor.min.css')
->add_jq_onready("
tikiToastEditor($jsonOptions);
");
return [];
}
/** Map between tiki lang codes and Toast (uses ISO codes)
*
* @param string $lang Tiki language code
*
* @return string mapped language code
* defaults empty if not found so not supported
*/
private function languageMapISO($lang)
{
$langMap = [
'ar' => 'ar', // Arabic = United Arab Emirates
//'bg' => 'bg', // Bulgarian
//'ca' => 'ca', // Catalan
'cn' => 'zh-CN', // China - Simplified Chinese
'cs' => 'cs-CZ', // Czech
//'cy' => 'cy', // Welsh
//'da' => 'da', // Danish
'de' => 'de-DE', // Germany - German
//'en-uk' => 'en-GB', // United Kingdom - English
'en' => 'en-US', // United States - English
'es' => 'es-ES', // Spain - Spanish
//'el' => 'el', // Greek
//'fa' => 'fa', // Farsi
'fi' => 'fi-FI', // Finnish
//'fj' => 'fj', // Fijian
'fr' => 'fr-FR', // France - French
'fy-NL' => 'nl', // Netherlands - Dutch
'gl' => 'gl-ES', // Galician
//'he' => 'he', // Israel - Hebrew
'hr' => 'hr-HR', // Croatian
//'id' => 'id', // Indonesian
//'is' => 'is', // Icelandic
'it' => 'it-IT', // Italy - Italian
//'iu' => 'iu', // Inuktitut
//'iu-ro' => 'iu-ro', // Inuktitut (Roman)
//'iu-iq' => 'iu-iq', // Iniunnaqtun
'ja' => 'ja-JP', // Japan - Japanese
'ko' => 'ko-KR', // Korean
//'hu' => 'hu', // Hungarian
//'lt' => 'lt', // Lithuanian
'nds' => 'de-DE', // Low German
'nl' => 'nl-NL', // Netherlands - Dutch
'no' => 'nb-NO', // Norway - Norwegian
'pl' => 'pl-PL', // Poland - Polish
'pt' => 'pt', // Portuguese
'pt-br' => 'pt-BR', // Brazil - Portuguese
//'ro' => 'ro', // Romanian
//'rm' => 'rm', // Romansh
'ru' => 'ru-RU', // Russia - Russian
//'sb' => 'sb', // Pijin Solomon
//'si' => 'si', // Sinhala
//'sk' => 'sk', // Slovak
//'sl' => 'sl', // Slovene
//'sq' => 'sq', // Albanian
//'sr-latn' => 'sr-latn', // Serbian Latin
'sv' => 'sv-SE', // Sweden - Swedish
//'tv' => 'tv', // Tuvaluansr-latn
'tr' => 'tr-TR', // Turkey - Turkish
'tw' => 'zh-TW', // Taiwan - Traditional Chinese
'uk' => 'uk-UA', // Ukrainian
//'vi' => 'vi', // Vietnamese
];
return isset($langMap[$lang]) ? $langMap[$lang] : '';
}
/** Map between tiki lang codes and ckeditor's (mostly the same)
*
* @param string $lang Tiki language code
......
......@@ -114,6 +114,7 @@ class Services_Edit_Controller
'preview_mode' => true,
'process_wiki_paragraphs' => ($prefs['wysiwyg_htmltowiki'] === 'y' || $info['wysiwyg'] == 'n'),
'page' => $page,
'is_markdown' => $input->is_markdown->int()
];
if (count($autoSaveIdParts) === 3 && ! empty($user) && $user === $autoSaveIdParts[0] && $autoSaveIdParts[1] === 'wiki_page') {
......@@ -133,6 +134,9 @@ class Services_Edit_Controller
TikiLib::lib('autosave')->get_autosave($input->editor_id->text(), $input->autoSaveId->text())
);
$data = $tikilib->convertAbsoluteLinksToRelative($data);
if ($input->is_markdown->int()) {
$data = "{syntax type=markdown}\r\n$data";
}
TikiLib::lib('smarty')->assign('diff_style', $diffstyle);
if ($diffstyle) {
if (! empty($info['created'])) {
......
......@@ -111,6 +111,7 @@ class Services_Edit_PluginController
$type = strtolower($input->type->word());
$index = $input->index->int();
$page = $input->page->pagename();
$isMarkdown = $input->isMarkdown->int();
$pluginArgs = $input->asArray('pluginArgs');
$bodyContent = $input->bodyContent->wikicontent();
$edit_icon = $input->edit_icon->int();
......@@ -254,6 +255,7 @@ class Services_Edit_PluginController
'bodyContent' => $bodyContent,
'edit_icon' => $edit_icon,
'selectedMod' => $selectedMod,
'isMarkdown' => $isMarkdown,
'info' => $info,
'title' => $info['name'],
......@@ -280,6 +282,29 @@ class Services_Edit_PluginController
return $util->replacePlugin($input);
}
/**
* Render a single plugin to html
*
* @param JitFilter $input
*
* @return array
*/
public function action_render(JitFilter $input): array
{
global $jitRequest;
$content = $input->markup->wikicontent();
$plugins = TikiLib::lib('parser')->find_plugins($content);
$pluginOutput = WikiParser_PluginOutput::wiki($content);
$html = $pluginOutput->toHtml();
return [
'html' => $html,
'plugins' => $plugins,
'pageName' => $jitRequest->page->pagename(),
];
}
/**
* Convert a trackerlist plugin to list
*
......
......@@ -34,6 +34,7 @@ class Services_Edit_Utilities
// Checking permission from plugin
$is_allowed = $parserlib->check_permission_from_plugin_params($params);
$referer = $_SERVER['HTTP_REFERER'];
$isMarkdown = $input->isMarkdown->int();
if (! $page || ! $type || ! $referer) {
throw new Services_Exception(tr('Missing parameters'));
......@@ -94,7 +95,16 @@ class Services_Edit_Utilities
$matches->getText(),
$message,
$user,
$tikilib->get_ip_address()
$tikilib->get_ip_address(),
'',
0,
'',
null,
null,
null,
'',
'',
$isMarkdown ? 'markdown' : 'tiki'
);
Feedback::success($message);
return [];
......
......@@ -2,7 +2,7 @@
namespace Tiki\Lib\core\Toolbar;
class ToolbarAdmin extends ToolbarItem
class ToolbarAdmin extends ToolbarUtilityItem
{
public function __construct()
......@@ -11,6 +11,8 @@ class ToolbarAdmin extends ToolbarItem
->setIconName('wrench')
->setIcon(tra('img/icons/wrench.png'))
->setWysiwygToken('admintoolbar')
->setMarkdownSyntax('admintoolbar')
->setMarkdownWysiwyg('admintoolbar')
->setType('admintoolbar')
->setClass('qt-admintoolbar');
}
......@@ -19,11 +21,9 @@ class ToolbarAdmin extends ToolbarItem
{
global $prefs;
if (! empty($this->wysiwyg)) {
$name = $this->wysiwyg; // temp
if ($prefs['feature_wysiwyg'] == 'y') {
$js = "admintoolbar();";
$this->setupCKEditorTool($js, $name, $this->label, $this->icon);
$this->setupCKEditorTool($this->getOnClick());
}
}
return $this->wysiwyg;
......
......@@ -11,6 +11,8 @@ class ToolbarBlock extends ToolbarInline // Will change in the future
$label = '';
$wysiwyg = '';
$syntax = '';
$markdown = '';
$markdown_wysiwyg = '';
switch ($tagName) {
case 'center':
......@@ -28,6 +30,8 @@ class ToolbarBlock extends ToolbarInline // Will change in the future
$iconname = 'horizontal-rule';
$wysiwyg = 'HorizontalRule';
$syntax = '---';
$markdown = '***';
$markdown_wysiwyg = 'hr';
break;
case 'pagebreak':
$label = tra('Page Break');
......@@ -52,6 +56,8 @@ class ToolbarBlock extends ToolbarInline // Will change in the future
$label = tra('Heading') . ' ' . $tagName[1];
$iconname = $tagName;
$syntax = str_repeat('!', $tagName[1]) . ' text';
$markdown = str_repeat('#', $tagName[1]) . ' text';
$markdown_wysiwyg = 'heading';
break;
case 'titlebar':
$label = tra('Title bar');
......@@ -73,6 +79,8 @@ class ToolbarBlock extends ToolbarInline // Will change in the future
->setWysiwygToken($wysiwyg)
->setIconName(! empty($iconname) ? $iconname : 'help')
->setSyntax($syntax)
->setMarkdownSyntax($markdown)
->setMarkdownWysiwyg($markdown_wysiwyg)
->setType('Block')
->setClass('qt-block');
......
......@@ -15,6 +15,7 @@ class ToolbarDialog extends ToolbarItem
global $prefs;
$tool_prefs = [];
$markdown_wysiwyg = '';
switch ($tagName) {
case 'tikilink':
......@@ -22,6 +23,7 @@ class ToolbarDialog extends ToolbarItem
$iconname = 'link';
$icon = tra('img/icons/page_link.png');
$wysiwyg = ''; // cke link dialog now adapted for wiki links
$markdown = ''; // TODO
$list = [
tra("Wiki Link"),
'<label for="tbWLinkDesc">' . tra("Show this text") . '</label>',
......@@ -88,6 +90,8 @@ class ToolbarDialog extends ToolbarItem
$label = tra('External Link');
$iconname = 'link-external';
$icon = tra('img/icons/world_link.png');
$markdown = ''; // TODO
$markdown_wysiwyg = 'link';
$list = [
tra('External Link'),
'<label for="tbLinkDesc">' . tra("Show this text") . '</label>',
......@@ -111,6 +115,8 @@ class ToolbarDialog extends ToolbarItem
$iconname = 'table';
$icon = tra('img/icons/table.png');
$wysiwyg = 'Table';
$markdown = ''; // TODO
$markdown_wysiwyg = 'table';
$label = tra('Table Builder');
$list = [
tra('Table Builder'),
......@@ -126,6 +132,7 @@ class ToolbarDialog extends ToolbarItem
$icon = tra('img/icons/find.png');
$iconname = 'search';
$wysiwyg = 'Find';
$markdown = ''; // TODO
$label = tra('Find Text');
$list = [
tra('Find Text'),
......@@ -146,6 +153,7 @@ class ToolbarDialog extends ToolbarItem
$icon = tra('img/icons/text_replace.png');
$iconname = 'repeat';
$wysiwyg = 'Replace';
$markdown = ''; // TODO
$label = tra('Text Replace');
$tool_prefs[] = 'feature_wiki_replace';
......@@ -175,6 +183,7 @@ class ToolbarDialog extends ToolbarItem
$tag = new self();
$tag->name = $tagName;
$tag->setWysiwygToken($wysiwyg)
->setMarkdownWysiwyg($markdown_wysiwyg)
->setLabel($label)
->setIconName(! empty($iconname) ? $iconname : 'help')
->setIcon(! empty($icon) ? $icon : 'img/icons/shading.png')
......@@ -236,7 +245,7 @@ class ToolbarDialog extends ToolbarItem
1 + $this->index
);
$onClick = str_replace('\'' . $this->domElementId . '\'', 'editor.name', $this->getOnClick());
$this->setupCKEditorTool($onClick, $this->wysiwyg, $this->label, $this->icon);
$this->setupCKEditorTool($onClick);
}
return $this->wysiwyg;
}
......
......@@ -71,7 +71,7 @@ class ToolbarFileGallery extends ToolbarUtilityItem
if (! empty($this->wysiwyg)) {
$exec_js = str_replace('&amp;', '&', $this->getOnClick());
$this->setupCKEditorTool($exec_js, $this->wysiwyg, $this->label, $this->icon);
$this->setupCKEditorTool($exec_js);
}
return $this->wysiwyg;
}
......
......@@ -11,6 +11,7 @@ class ToolbarHelptool extends ToolbarUtilityItem
$this->setLabel(tra('Wiki Help'))
->setIcon('img/icons/help.png')
->setType('Helptool')
->setWysiwygToken('tikihelp')
->setClass('qt-help');
}
......@@ -59,7 +60,7 @@ class ToolbarHelptool extends ToolbarUtilityItem
$js = '$.openModal({show: true, remote: "' . $servicelib->getUrl($params) . '" + editor.name});';
$this->setupCKEditorTool($js, $name, $this->label, $this->icon);
$this->setupCKEditorTool($js);
return $name;
}
......
......@@ -4,10 +4,12 @@ namespace Tiki\Lib\core\Toolbar;
class ToolbarInline extends ToolbarItem
{
protected string $syntax;
public static function fromName($tagName): ?ToolbarItem
{
$markdown = '';
$markdown_wysiwyg = '';
switch ($tagName) {
case 'bold':
$label = tra('Bold');
......@@ -15,6 +17,8 @@ class ToolbarInline extends ToolbarItem
$iconname = 'bold';
$wysiwyg = 'Bold';
$syntax = '__text__';
$markdown = '__text__';
$markdown_wysiwyg = 'bold';
break;
case 'italic':
$label = tra('Italic');
......@@ -22,6 +26,8 @@ class ToolbarInline extends ToolbarItem
$iconname = 'italic';
$wysiwyg = 'Italic';
$syntax = "''text''";
$markdown = '_text_';
$markdown_wysiwyg = 'italic';
break;
case 'underline':
$label = tra('Underline');
......@@ -36,6 +42,8 @@ class ToolbarInline extends ToolbarItem
$iconname = 'strikethrough';
$wysiwyg = 'Strike';
$syntax = '--text--';
$markdown = '~~text~~';
$markdown_wysiwyg = 'strike';
break;
case 'code':
$label = tra('Code');
......@@ -43,6 +51,8 @@ class ToolbarInline extends ToolbarItem
$iconname = 'code';
$wysiwyg = 'Code';
$syntax = '-+text+-';
$markdown = '`text`';
$markdown_wysiwyg = 'code';
break;
case 'nonparsed':
$label = tra('Non-parsed (wiki syntax does not apply)');
......@@ -61,32 +71,33 @@ class ToolbarInline extends ToolbarItem
->setIconName(! empty($iconname) ? $iconname : 'help')
->setIcon(! empty($icon) ? $icon : 'img/icons/shading.png')
->setSyntax($syntax)
->setMarkdownSyntax($markdown)
->setMarkdownWysiwyg($markdown_wysiwyg)
->setType('Inline')
->setClass('qt-inline');
return $tag;
}
public function getSyntax(): string
{
return $this->syntax;
}
public function setSyntax(string $syntax): ToolbarItem
/**
* @return string
*/
public function getOnClick(): string
{
$this->syntax = $syntax;
return $this;
return 'insertAt(\'' . $this->domElementId . '\', \'' .
addslashes(
htmlentities($this->syntax, ENT_COMPAT, 'UTF-8')
) . '\');';
}
/**
* @return string
*/
public function getOnClick(): string
public function getOnClickMarkdown(): string
{
return 'insertAt(\'' . $this->domElementId . '\', \'' .
addslashes(
htmlentities($this->syntax, ENT_COMPAT, 'UTF-8')
htmlentities($this->markdown, ENT_COMPAT, 'UTF-8')
) . '\');';
}
}
......@@ -14,6 +14,10 @@ abstract class ToolbarItem
protected string $type = '';
protected string $domElementId = '';
protected string $class = '';
protected string $syntax = '';
protected string $markdown = '';
protected string $markdown_wysiwyg = '';
private array $requiredPrefs = [];
/**
* @return string
......@@ -53,7 +57,6 @@ abstract class ToolbarItem
return $this;
}
private array $requiredPrefs = [];
public static function getTag(string $tagName, bool $wysiwyg = false, bool $is_html = false): ?ToolbarItem
{
......@@ -380,8 +383,45 @@ abstract class ToolbarItem
);
}
public function getSyntax(): string
{