Improved HTML parser + editor integration
authorAlexander Ebert <ebert@woltlab.com>
Thu, 26 May 2016 08:35:06 +0000 (10:35 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 26 May 2016 08:35:13 +0000 (10:35 +0200)
17 files changed:
com.woltlab.wcf/templates/messageFormSettings.tpl
com.woltlab.wcf/templates/messageFormTabs.tpl
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Metacode.js [new file with mode: 0644]
wcfsetup/install/files/lib/data/IMessageQuickReplyAction.class.php
wcfsetup/install/files/lib/form/MessageForm.class.php
wcfsetup/install/files/lib/system/bbcode/HtmlBBCodeParser.class.php
wcfsetup/install/files/lib/system/html/input/HtmlInputProcessor.class.php
wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeProcessor.class.php
wcfsetup/install/files/lib/system/html/input/node/IHtmlInputNodeProcessor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/html/node/HtmlNodeProcessor.class.php
wcfsetup/install/files/lib/system/html/node/IHtmlNodeProcessor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/MessageFormSettingsHandler.class.php [deleted file]
wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php
wcfsetup/install/files/lib/system/template/plugin/LangCompilerTemplatePlugin.class.php
wcfsetup/install/files/lib/util/DOMUtil.class.php

index 2da8f1a448fef5b108ebaa29788ed73a7600fd69..fce986a290efc3b42ba449a1680a5b2144b69b48 100644 (file)
@@ -1,41 +1,10 @@
-<div id="settings_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}" class="settingsContent messageTabMenuContent">
-       <dl class="wide">
-               {if $__wcf->getSession()->getPermission($permissionCanUseBBCodes)}
-                       <dt></dt>
-                       <dd>
-                               <label><input id="preParse" name="preParse" type="checkbox" value="1"{if $preParse} checked="checked"{/if} /> {lang}wcf.message.settings.preParse{/lang}</label>
-                               <small>{lang}wcf.message.settings.preParse.description{/lang}</small>
-                       </dd>
-               {/if}
-               {if MODULE_SMILEY && $__wcf->getSession()->getPermission($permissionCanUseSmilies)}
-                       <dt></dt>
-                       <dd>
-                               <label><input id="enableSmilies" name="enableSmilies" type="checkbox" value="1"{if $enableSmilies} checked="checked"{/if} /> {lang}wcf.message.settings.enableSmilies{/lang}</label>
-                               <small>{lang}wcf.message.settings.enableSmilies.description{/lang}</small>
-                       </dd>
-               {/if}
-               {if $__wcf->getSession()->getPermission($permissionCanUseBBCodes)}
-                       <dt></dt>
-                       <dd>
-                               <label><input id="enableBBCodes" name="enableBBCodes" type="checkbox" value="1"{if $enableBBCodes} checked="checked"{/if} /> {lang}wcf.message.settings.enableBBCodes{/lang}</label>
-                               <small>{lang}wcf.message.settings.enableBBCodes.description{/lang}</small>
-                       </dd>
-               {/if}
-               {if $__wcf->getSession()->getPermission($permissionCanUseHtml)}
-                       <dt></dt>
-                       <dd>
-                               <label><input id="enableHtml" name="enableHtml" type="checkbox" value="1"{if $enableHtml} checked="checked"{/if} /> {lang}wcf.message.settings.enableHtml{/lang}</label>
-                               <small>{lang}wcf.message.settings.enableHtml.description{/lang}</small>
-                       </dd>
-               {/if}
-               {if MODULE_USER_SIGNATURE && !$showSignatureSetting|empty && $__wcf->user->userID}
-                       <dt></dt>
-                       <dd>
-                               <label><input id="showSignature" name="showSignature" type="checkbox" value="1"{if $showSignature} checked="checked"{/if} /> {lang}wcf.message.settings.showSignature{/lang}</label>
-                               <small>{lang}wcf.message.settings.showSignature.description{/lang}</small>
-                       </dd>
-               {/if}
-               
-               {event name='settings'}
-       </dl>
-</div>
+{capture assign='__messageFormSettings'}
+       {hascontent}
+               <div id="settings_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}" class="settingsContent messageTabMenuContent">
+                       <dl class="wide">
+                               {content}{event name='settings'}{/content}
+                       </dl>
+               </div>
+       {/hascontent}
+{/capture}
+{assign var='__messageFormSettings' value=$__messageFormSettings|trim}
index ad5e670912854041be2ed88a8a44bd6c99abec84..188a0f06de9a248ed3b579d879e4c6ea70e56116 100644 (file)
@@ -1,18 +1,21 @@
+{* the settings template does not generate direct ouput anymore, but captures it content *}
+{include file='messageFormSettings'}
+
 <div class="messageTabMenu" data-preselect="{if $preselectTabMenu|isset}{$preselectTabMenu}{else}true{/if}" data-wysiwyg-container-id="{if $wysiwygContainerID|isset}{$wysiwygContainerID}{else}text{/if}">
        <nav class="messageTabMenuNavigation jsOnly">
                <ul>
-                       {if MODULE_SMILEY && $__wcf->getSession()->getPermission($permissionCanUseSmilies) && $smileyCategories|count}<li data-name="smilies"><a><span class="icon icon16 fa-smile-o"></span> <span>{lang}wcf.message.smilies{/lang}</span></a></li>{/if}
+                       {if MODULE_SMILEY && !$smileyCategories|empty}<li data-name="smilies"><a><span class="icon icon16 fa-smile-o"></span> <span>{lang}wcf.message.smilies{/lang}</span></a></li>{/if}
                        {if MODULE_ATTACHMENT && !$attachmentHandler|empty && $attachmentHandler->canUpload()}<li data-name="attachments"><a><span class="icon icon16 fa-paperclip"></span> <span>{lang}wcf.attachment.attachments{/lang}</span></a></li>{/if}
-                       <li data-name="settings"><a><span class="icon icon16 fa-cog"></span> <span>{lang}wcf.message.settings{/lang}</span></a></li>
+                       {if $__messageFormSettings}<li data-name="settings"><a><span class="icon icon16 fa-cog"></span> <span>{lang}wcf.message.settings{/lang}</span></a></li>{/if}
                        {if $__showPoll|isset && $__showPoll}<li data-name="poll"><a><span class="icon icon16 fa-bar-chart"></span> <span>{lang}wcf.poll.management{/lang}</span></a></li>{/if}
                        {event name='tabMenuTabs'}
                </ul>
        </nav>
        
-       {if MODULE_SMILEY && $__wcf->getSession()->getPermission($permissionCanUseSmilies) && $smileyCategories|count}{include file='messageFormSmilies'}{/if}
+       {if MODULE_SMILEY && !$smileyCategories|empty}{include file='messageFormSmilies'}{/if}
        {if MODULE_ATTACHMENT && !$attachmentHandler|empty && $attachmentHandler->canUpload()}{include file='messageFormAttachments'}{/if}
        
-       {include file='messageFormSettings'}
+       {if $__messageFormSettings}{@$__messageFormSettings}{/if}
        {include file='__messageFormPoll'}
        
        {event name='tabMenuContents'}
index 943c2cc7b4211806e84c81f6668462735d0f8f86..1529901c03f135b62b2f4d6010ca4a2e2d6e4cf1 100644 (file)
@@ -8,74 +8,78 @@
        }
 </style>
 <script data-relocate="true">
-(function() {
-       var buttons = [], buttonOptions = [];
-       {include file='wysiwygToolbar'}
-       
-       var elementId = '{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}';
-       var callbackIdentifier = 'Redactor2_' + elementId;
-       
-       WCF.System.Dependency.Manager.setup(callbackIdentifier, function() {
-               // TODO: Should the media stuff be here?
-               {include file='mediaJavaScript'}
+require(['WoltLab/WCF/Ui/Redactor/Metacode'], function(UiRedactorMetacode) {
+       (function() {
+               var buttons = [], buttonOptions = [];
+               {include file='wysiwygToolbar'}
                
-               var element = elById(elementId);
-               var autosave = elData(element, 'autosave') || '';
-               if (autosave) {
-                       element.removeAttribute('data-autosave');
-               }
+               var elementId = '{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}';
+               var callbackIdentifier = 'Redactor2_' + elementId;
                
-               var config = {
-                       buttons: buttons,
-                       minHeight: 200,
-                       plugins: ['alignment', 'source', 'table', 'WoltLabColor', 'WoltLabDropdown', 'WoltLabEvent', 'WoltLabLink', 'WoltLabQuote', 'WoltLabSize'],
-                       toolbarFixed: false,
-                       woltlab: {
-                               autosave: autosave,
-                               buttons: buttonOptions
+               WCF.System.Dependency.Manager.setup(callbackIdentifier, function() {
+                       // TODO: Should the media stuff be here?
+                       {include file='mediaJavaScript'}
+                       
+                       var element = elById(elementId);
+                       UiRedactorMetacode.convert(element);
+                       
+                       var autosave = elData(element, 'autosave') || '';
+                       if (autosave) {
+                               element.removeAttribute('data-autosave');
                        }
-               };
-               
-               // user mentions
-               if (elDataBool(element, 'support-mention')) {
-                       config.plugins.push('WoltLabMention');
-               }
-               
-               // media
-               {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}
-                       config.plugins.push('WoltLabMedia');
-               {/if}
-               
-               // load the button plugin last to ensure all buttons have been initialized
-               // already and we can safely add all icons
-               config.plugins.push('WoltLabButton');
-               
-               $(element).redactor(config);
-       });
-               
-       head.load([
-               {* Imperavi *}
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/redactor.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/alignment.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/source.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/table.js?v={@LAST_UPDATE_TIME}',
-               
-               {* WoltLab *}
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabButton.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabColor.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabDropdown.js?v={@LAST_UPDATE_TIME}', 
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabEvent.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabLink.js?v={@LAST_UPDATE_TIME}',
-               {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}'{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabMedia.js?v={@LAST_UPDATE_TIME}',{/if}
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabMention.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabQuote.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabSize.js?v={@LAST_UPDATE_TIME}'
-               
-               ], function() {
-                       WCF.System.Dependency.Manager.invoke(callbackIdentifier);
-               }
-       );
-})();
+                       
+                       var config = {
+                               buttons: buttons,
+                               minHeight: 200,
+                               plugins: ['alignment', 'source', 'table', 'WoltLabColor', 'WoltLabDropdown', 'WoltLabEvent', 'WoltLabLink', 'WoltLabQuote', 'WoltLabSize'],
+                               toolbarFixed: false,
+                               woltlab: {
+                                       autosave: autosave,
+                                       buttons: buttonOptions
+                               }
+                       };
+                       
+                       // user mentions
+                       if (elDataBool(element, 'support-mention')) {
+                               config.plugins.push('WoltLabMention');
+                       }
+                       
+                       // media
+                       {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}
+                               config.plugins.push('WoltLabMedia');
+                       {/if}
+                       
+                       // load the button plugin last to ensure all buttons have been initialized
+                       // already and we can safely add all icons
+                       config.plugins.push('WoltLabButton');
+                       
+                       $(element).redactor(config);
+               });
+                       
+               head.load([
+                       {* Imperavi *}
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/redactor.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/alignment.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/source.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/table.js?v={@LAST_UPDATE_TIME}',
+                       
+                       {* WoltLab *}
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabButton.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabColor.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabDropdown.js?v={@LAST_UPDATE_TIME}', 
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabEvent.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabLink.js?v={@LAST_UPDATE_TIME}',
+                       {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}'{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabMedia.js?v={@LAST_UPDATE_TIME}',{/if}
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabMention.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabQuote.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabSize.js?v={@LAST_UPDATE_TIME}'
+                       
+                       ], function() {
+                               WCF.System.Dependency.Manager.invoke(callbackIdentifier);
+                       }
+               );
+       })();
+});
 </script>
 
 {*
index 4e582ca9902589d396512c934442edd8e9dfe0ff..9cd587f56ad143a9e2ce5d0dfef83e311881796c 100644 (file)
@@ -166,11 +166,11 @@ define(['Environment', 'StringUtil'], function(Environment, StringUtil) {
                 * @param       {Element}       parentEl        future containing element
                 */
                prepend: function(el, parentEl) {
-                       if (parentEl.childElementCount === 0) {
+                       if (parentEl.childNodes.length === 0) {
                                parentEl.appendChild(el);
                        }
                        else {
-                               parentEl.insertBefore(el, parentEl.children[0]);
+                               parentEl.insertBefore(el, parentEl.childNodes[0]);
                        }
                },
                
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Metacode.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Metacode.js
new file mode 100644 (file)
index 0000000..a988193
--- /dev/null
@@ -0,0 +1,148 @@
+/**
+ * Converts `<woltlab-metacode>` into the bbcode representation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Redactor/Metacode
+ */
+define(['Dom/Util'], function(DomUtil) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLab/WCF/Ui/Redactor/Metacode
+        */
+       return {
+               /**
+                * Converts `<woltlab-metacode>` into the bbcode representation.
+                * 
+                * @param       {Element}       element         textarea element
+                */
+               convert: function(element) {
+                       var div = elCreate('div');
+                       div.innerHTML = element.textContent;
+                       
+                       var attributes, metacode, metacodes = elByTag('woltlab-metacode', div), name, tagClose, tagOpen;
+                       while (metacodes.length) {
+                               metacode = metacodes[0];
+                               name = elData(metacode, 'name');
+                               attributes = elData(metacode, 'attributes');
+                               
+                               tagOpen = this._getOpeningTag(name, attributes);
+                               tagClose = this._getClosingTag(name);
+                               
+                               if (metacode.parentNode === div) {
+                                       DomUtil.prepend(tagOpen, this._getFirstParagraph(metacode));
+                                       this._getLastParagraph(metacode).appendChild(tagClose);
+                               }
+                               else {
+                                       DomUtil.prepend(tagOpen, metacode);
+                                       metacode.appendChild(tagClose);
+                               }
+                               
+                               DomUtil.unwrapChildNodes(metacode);
+                       }
+                       
+                       element.textContent = div.innerHTML;
+               },
+               
+               /**
+                * Returns a text node representing the opening bbcode tag.
+                * 
+                * @param       {string}        name            bbcode tag
+                * @param       {string}        attributes      base64- and JSON-encoded attributes
+                * @returns     {Text}          text node containing the opening bbcode tag
+                * @protected
+                */
+               _getOpeningTag: function(name, attributes) {
+                       try {
+                               attributes = JSON.parse(atob(attributes));
+                       }
+                       catch (e) { /* invalid base64 data or invalid json */ }
+                       
+                       if (!Array.isArray(attributes)) {
+                               attributes = [];
+                       }
+                       
+                       var buffer = '[' + name;
+                       if (attributes.length) {
+                               buffer += '=' + attributes.join(',');
+                       }
+                       
+                       return document.createTextNode(buffer + ']');
+               },
+               
+               /**
+                * Returns a text node representing the closing bbcode tag.
+                * 
+                * @param       {string}        name            bbcode tag
+                * @returns     {Text}          text node containing the closing bbcode tag
+                * @protected
+                */
+               _getClosingTag: function(name) {
+                       return document.createTextNode('[/' + name + ']');
+               },
+               
+               /**
+                * Returns the first paragraph of provided element. If there are no children or
+                * the first child is not a paragraph, a new paragraph is created and inserted
+                * as first child.
+                * 
+                * @param       {Element}       element         metacode element
+                * @returns     {Element}       paragraph that is the first child of provided element
+                * @protected
+                */
+               _getFirstParagraph: function (element) {
+                       var firstChild, paragraph;
+                       
+                       if (element.childElementCount === 0) {
+                               paragraph = elCreate('p');
+                               element.appendChild(paragraph);
+                       }
+                       else {
+                               firstChild = element.children[0];
+                               
+                               if (firstChild.nodeName === 'P') {
+                                       paragraph = firstChild;
+                               }
+                               else {
+                                       paragraph = elCreate('p');
+                                       element.insertBefore(paragraph, firstChild);
+                               }
+                       }
+                       
+                       return paragraph;
+               },
+               
+               /**
+                * Returns the last paragraph of provided element. If there are no children or
+                * the last child is not a paragraph, a new paragraph is created and inserted
+                * as last child.
+                * 
+                * @param       {Element}       element         metacode element
+                * @returns     {Element}       paragraph that is the last child of provided element
+                * @protected
+                */
+               _getLastParagraph: function (element) {
+                       var count = element.childElementCount, lastChild, paragraph;
+                       
+                       if (count === 0) {
+                               paragraph = elCreate('p');
+                               element.appendChild(paragraph);
+                       }
+                       else {
+                               lastChild = element.children[count - 1];
+                               
+                               if (lastChild.nodeName === 'P') {
+                                       paragraph = lastChild;
+                               }
+                               else {
+                                       paragraph = elCreate('p');
+                                       element.appendChild(paragraph);
+                               }
+                       }
+                       
+                       return paragraph;
+               }
+       };
+});
index 6e72ff5d5d008d67b3700e840d3ba38eb7ab85b7..03d592c0384855bd3eba53fc7f0a6d1ace2fdfec 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\data;
+use wcf\system\html\input\HtmlInputProcessor;
 
 /**
  * Default interface for actions implementing quick reply.
@@ -15,23 +16,31 @@ interface IMessageQuickReplyAction {
        /**
         * Creates a new message object.
         * 
-        * @return      \wcf\data\DatabaseObject
+        * @return      DatabaseObject
         */
        public function create();
        
+       /**
+        * Returns the current html input processor or a new one if `$message` is not null.
+        * 
+        * @param       string|null     $message        source message
+        * @return      HtmlInputProcessor
+        */
+       public function getHtmlInputProcessor($message = null);
+       
        /**
         * Returns a message list object.
         * 
-        * @param       \wcf\data\DatabaseObject                $container
-        * @param       integer                         $lastMessageTime
-        * @return      \wcf\data\DatabaseObjectList
+        * @param       DatabaseObject          $container
+        * @param       integer                 $lastMessageTime
+        * @return      DatabaseObjectList
         */
        public function getMessageList(DatabaseObject $container, $lastMessageTime);
        
        /**
         * Returns page no for given container object.
         * 
-        * @param       \wcf\data\DatabaseObject                $container
+        * @param       DatabaseObject          $container
         * @return      array
         */
        public function getPageNo(DatabaseObject $container);
@@ -39,8 +48,8 @@ interface IMessageQuickReplyAction {
        /**
         * Returns the redirect url.
         * 
-        * @param       \wcf\data\DatabaseObject                $container
-        * @param       \wcf\data\DatabaseObject                $message
+        * @param       DatabaseObject          $container
+        * @param       DatabaseObject          $message
         * @return      string
         */
        public function getRedirectUrl(DatabaseObject $container, DatabaseObject $message);
@@ -48,10 +57,10 @@ interface IMessageQuickReplyAction {
        /**
         * Validates the message.
         * 
-        * @param       \wcf\data\DatabaseObject                $container
-        * @param       string                          $message
+        * @param       DatabaseObject          $container
+        * @param       HtmlInputProcessor      $htmlInputProcessor
         */
-       public function validateMessage(DatabaseObject $container, $message);
+       public function validateMessage(DatabaseObject $container, HtmlInputProcessor $htmlInputProcessor);
        
        /**
         * Creates a new message and returns it.
@@ -63,7 +72,7 @@ interface IMessageQuickReplyAction {
        /**
         * Validates the container object for quick reply.
         * 
-        * @param       \wcf\data\DatabaseObject                $container
+        * @param       DatabaseObject          $container
         */
        public function validateContainer(DatabaseObject $container);
        
index 23887d5d8823ae7ccc09f05d5d10debacc198b2c..08e6fa3d75e0318bb838a3ba3ba6869bd99e8cb0 100644 (file)
@@ -36,9 +36,9 @@ abstract class MessageForm extends AbstractCaptchaForm {
        
        /**
         * attachment handler
-        * @var \wcf\system\attachment\AttachmentHandler
+        * @var AttachmentHandler
         */
-       public $attachmentHandler = null;
+       public $attachmentHandler;
        
        /**
         * object id for attachments
@@ -70,18 +70,6 @@ abstract class MessageForm extends AbstractCaptchaForm {
         */
        public $defaultSmilies = [];
        
-       /**
-        * enables bbcodes
-        * @var boolean
-        */
-       public $enableBBCodes = 1;
-       
-       /**
-        * enables html
-        * @var boolean
-        */
-       public $enableHtml = 0;
-       
        /**
         * enables multilingualism
         * @var boolean
@@ -89,16 +77,15 @@ abstract class MessageForm extends AbstractCaptchaForm {
        public $enableMultilingualism = false;
        
        /**
-        * enables smilies
-        * @var boolean
+        * @var HtmlInputProcessor
         */
-       public $enableSmilies = 1;
+       public $htmlInputProcessor;
        
        /**
         * content language id
         * @var integer
         */
-       public $languageID = null;
+       public $languageID;
        
        /**
         * maximum text length
@@ -106,42 +93,6 @@ abstract class MessageForm extends AbstractCaptchaForm {
         */
        public $maxTextLength = 0;
        
-       /**
-        * pre parses the message
-        * @var boolean
-        */
-       public $preParse = 1;
-       
-       /**
-        * required permission to use BBCodes
-        * @var boolean
-        */
-       public $permissionCanUseBBCodes = 'user.message.canUseBBCodes';
-       
-       /**
-        * required permission to use HTML
-        * @var boolean
-        */
-       public $permissionCanUseHtml = 'user.message.canUseHtml';
-       
-       /**
-        * required permission to use smilies
-        * @var boolean
-        */
-       public $permissionCanUseSmilies = 'user.message.canUseSmilies';
-       
-       /**
-        * shows the signature
-        * @var boolean
-        */
-       public $showSignature = 0;
-       
-       /**
-        * enables the 'showSignature' setting
-        * @var boolean
-        */
-       public $showSignatureSetting = 1;
-       
        /**
         * list of smiley categories
         * @var SmileyCategory[]
@@ -204,14 +155,6 @@ abstract class MessageForm extends AbstractCaptchaForm {
                if (isset($_POST['subject'])) $this->subject = StringUtil::trim(MessageUtil::stripCrap($_POST['subject']));
                if (isset($_POST['text'])) $this->text = StringUtil::trim(MessageUtil::stripCrap($_POST['text']));
                
-               // settings
-               $this->enableSmilies = $this->enableHtml = $this->enableBBCodes = $this->preParse = $this->showSignature = 0;
-               if (isset($_POST['preParse'])) $this->preParse = intval($_POST['preParse']);
-               if (isset($_POST['enableSmilies']) && WCF::getSession()->getPermission($this->permissionCanUseSmilies)) $this->enableSmilies = intval($_POST['enableSmilies']);
-               if (isset($_POST['enableHtml']) && WCF::getSession()->getPermission($this->permissionCanUseHtml)) $this->enableHtml = intval($_POST['enableHtml']);
-               if (isset($_POST['enableBBCodes']) && WCF::getSession()->getPermission($this->permissionCanUseBBCodes)) $this->enableBBCodes = intval($_POST['enableBBCodes']);
-               if (isset($_POST['showSignature'])) $this->showSignature = intval($_POST['showSignature']);
-               
                // multilingualism
                if (isset($_POST['languageID'])) $this->languageID = intval($_POST['languageID']);
        }
@@ -267,6 +210,12 @@ abstract class MessageForm extends AbstractCaptchaForm {
                        throw new UserInputException('text', 'tooLong');
                }
                
+               $this->htmlInputProcessor = new HtmlInputProcessor();
+               $this->htmlInputProcessor->process($this->text);
+               
+               // TODO: add checks for disallowed bbcodes and stuff
+               $this->htmlInputProcessor->validate();
+               
                /*if ($this->enableBBCodes && $this->allowedBBCodesPermission) {
                        $disallowedBBCodes = BBCodeParser::getInstance()->validateBBCodes($this->text, ArrayUtil::trim(explode(',', WCF::getSession()->getPermission($this->allowedBBCodesPermission))));
                        if (!empty($disallowedBBCodes)) {
@@ -305,8 +254,7 @@ abstract class MessageForm extends AbstractCaptchaForm {
        public function save() {
                parent::save();
                
-               $htmlInputProcessor = new HtmlInputProcessor();
-               $this->text = $htmlInputProcessor->process($this->text);
+               $this->text = $this->htmlInputProcessor->getHtml();
                
                // parse URLs
                /* TODO
@@ -338,11 +286,6 @@ abstract class MessageForm extends AbstractCaptchaForm {
                }
                
                if (empty($_POST)) {
-                       $this->enableBBCodes = (ENABLE_BBCODES_DEFAULT_VALUE && WCF::getSession()->getPermission($this->permissionCanUseBBCodes)) ? 1 : 0;
-                       $this->enableHtml = (ENABLE_HTML_DEFAULT_VALUE && WCF::getSession()->getPermission($this->permissionCanUseHtml)) ? 1 : 0;
-                       $this->enableSmilies = (ENABLE_SMILIES_DEFAULT_VALUE && WCF::getSession()->getPermission($this->permissionCanUseSmilies)) ? 1 : 0;
-                       $this->preParse = PRE_PARSE_DEFAULT_VALUE;
-                       $this->showSignature = SHOW_SIGNATURE_DEFAULT_VALUE;
                        $this->languageID = WCF::getLanguage()->languageID;
                }
                
@@ -358,7 +301,7 @@ abstract class MessageForm extends AbstractCaptchaForm {
                        }
                }
                
-               if ($this->enableBBCodes && $this->allowedBBCodesPermission) {
+               if ($this->allowedBBCodesPermission) {
                        BBCodeHandler::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission($this->allowedBBCodesPermission)));
                }
        }
@@ -376,17 +319,8 @@ abstract class MessageForm extends AbstractCaptchaForm {
                        'attachmentParentObjectID' => $this->attachmentParentObjectID,
                        'availableContentLanguages' => $this->availableContentLanguages,
                        'defaultSmilies' => $this->defaultSmilies,
-                       'enableBBCodes' => $this->enableBBCodes,
-                       'enableHtml' => $this->enableHtml,
-                       'enableSmilies' => $this->enableSmilies,
                        'languageID' => ($this->languageID ?: 0),
                        'maxTextLength' => $this->maxTextLength,
-                       'permissionCanUseBBCodes' => $this->permissionCanUseBBCodes,
-                       'permissionCanUseHtml' => $this->permissionCanUseHtml,
-                       'permissionCanUseSmilies' => $this->permissionCanUseSmilies,
-                       'preParse' => $this->preParse,
-                       'showSignature' => $this->showSignature,
-                       'showSignatureSetting' => $this->showSignatureSetting,
                        'smileyCategories' => $this->smileyCategories,
                        'subject' => $this->subject,
                        'text' => $this->text,
index cd2744a4ad790905fd9d7f054fd91cdfe2d0ce80..589db826481a0b724dfe0c1935724e270651eb24 100644 (file)
@@ -256,6 +256,15 @@ class HtmlBBCodeParser extends BBCodeParser {
                
                $attributes = '';
                if (!empty($tag['attributes'])) {
+                       // strip outer quote tags
+                       $tag['attributes'] = array_map(function($attribute) {
+                               if (preg_match('~^([\'"])(?P<content>.*)(\1)$~', $attribute, $matches)) {
+                                       return $matches['content'];
+                               }
+                               
+                               return $attribute;
+                       }, $tag['attributes']);
+                       
                        // uses base64 encoding to avoid an "escape" nightmare
                        $attributes = ' data-attributes="' . base64_encode(JSON::encode($tag['attributes'])) . '"';
                }
index 7847d442fedb54b9dbf7cbb0e92ddc792230fbb8..0f1ae9e0ae3afd9a25ab7adb05be52e8e0171934 100644 (file)
@@ -4,6 +4,8 @@ use wcf\system\bbcode\HtmlBBCodeParser;
 use wcf\system\html\input\filter\IHtmlInputFilter;
 use wcf\system\html\input\filter\MessageHtmlInputFilter;
 use wcf\system\html\input\node\HtmlInputNodeProcessor;
+use wcf\system\html\input\node\IHtmlInputNodeProcessor;
+use wcf\system\html\node\IHtmlNodeProcessor;
 use wcf\util\StringUtil;
 
 /**
@@ -11,13 +13,15 @@ use wcf\util\StringUtil;
  * @since      2.2
  */
 class HtmlInputProcessor {
+       protected $embeddedContent = [];
+       
        /**
         * @var IHtmlInputFilter
         */
        protected $htmlInputFilter;
        
        /**
-        * @var HtmlInputNodeProcessor
+        * @var IHtmlInputNodeProcessor
         */
        protected $htmlInputNodeProcessor;
        
@@ -34,16 +38,19 @@ class HtmlInputProcessor {
                // pre-parse HTML
                $this->getHtmlInputNodeProcessor()->load($html);
                $this->getHtmlInputNodeProcessor()->process();
-               
-               return $this->getHtmlInputNodeProcessor()->getHtml();
+               $this->embeddedContent = $this->getHtmlInputNodeProcessor()->getEmbeddedContent();
        }
        
-       public function setHtmlInputFilter(IHtmlInputFilter $htmlInputFilter) {
-               $this->htmlInputFilter = $htmlInputFilter;
+       public function validate() {
+               // TODO
+       }
+       
+       public function getHtml() {
+               return $this->getHtmlInputNodeProcessor()->getHtml();
        }
        
        /**
-        * @return      IHtmlInputFilter|MessageHtmlInputFilter
+        * @return      IHtmlInputFilter
         */
        public function getHtmlInputFilter() {
                if ($this->htmlInputFilter === null) {
@@ -53,12 +60,12 @@ class HtmlInputProcessor {
                return $this->htmlInputFilter;
        }
        
-       public function setHtmlInputNodeProcessor(HtmlInputNodeProcessor $htmlInputNodeProcessor) {
-               $this->htmlInputNodeProcessor = $htmlInputNodeProcessor;
+       public function setHtmlInputFilter(IHtmlInputFilter $htmlInputFilter) {
+               $this->htmlInputFilter = $htmlInputFilter;
        }
        
        /**
-        * @return      HtmlInputNodeProcessor
+        * @return IHtmlInputNodeProcessor
         */
        public function getHtmlInputNodeProcessor() {
                if ($this->htmlInputNodeProcessor === null) {
@@ -67,4 +74,12 @@ class HtmlInputProcessor {
                
                return $this->htmlInputNodeProcessor;
        }
+       
+       public function setHtmlInputNodeProcessor(IHtmlNodeProcessor $htmlInputNodeProcessor) {
+               $this->htmlInputNodeProcessor = $htmlInputNodeProcessor;
+       }
+       
+       public function getEmbeddedContent() {
+               return $this->embeddedContent;
+       }
 }
index c4c6f9f464266ea9046d51e891b7561b7844c7fa..a1d6ae62219c60452032e251be794cc0533cc6d1 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\html\input\node;
+use wcf\system\event\EventHandler;
 use wcf\system\html\node\HtmlNodeProcessor;
 use wcf\util\DOMUtil;
 
@@ -7,7 +8,9 @@ use wcf\util\DOMUtil;
  * TOOD documentation
  * @since      2.2
  */
-class HtmlInputNodeProcessor extends HtmlNodeProcessor {
+class HtmlInputNodeProcessor extends HtmlNodeProcessor implements IHtmlInputNodeProcessor {
+       protected $embeddedContent = [];
+       
        // TODO: this should include other tags
        protected $emptyTags = ['em', 'strong', 'u'];
        
@@ -15,14 +18,53 @@ class HtmlInputNodeProcessor extends HtmlNodeProcessor {
        protected $mergeTags = ['em', 'strong', 'u'];
        
        public function process() {
+               EventHandler::getInstance()->fireAction($this, 'beforeProcess');
+               
+               $this->embeddedContent = [];
+               
                // process metacode markers first
                $this->invokeHtmlNode(new HtmlInputNodeWoltlabMetacodeMarker());
                
                // handle static converters
                $this->invokeHtmlNode(new HtmlInputNodeWoltlabMetacode());
                
+               // extract embedded content
+               $this->parseEmbeddedContent();
+               
                // remove empty elements and join identical siblings if appropriate
                $this->cleanup();
+               
+               EventHandler::getInstance()->fireAction($this, 'afterProcess');
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getEmbeddedContent() {
+               return $this->embeddedContent;
+       }
+       
+       public function addEmbeddedContent($type, array $data) {
+               $this->embeddedContent[$type] = $data;
+       }
+       
+       protected function parseEmbeddedContent() {
+               // handle `woltlab-metacode`
+               $elements = $this->getDocument()->getElementsByTagName('woltlab-metacode');
+               $metacodesByName = [];
+               for ($i = 0, $length = $elements->length; $i < $length; $i++) {
+                       /** @var \DOMElement $element */
+                       $element = $elements->item($i);
+                       $name = $element->getAttribute('data-name');
+                       $attributes = $this->parseAttributes($element->getAttribute('data-attributes'));
+                       
+                       if (!isset($metacodesByName[$name])) $metacodesByName[$name] = [];
+                       $metacodesByName[$name][] = $attributes;
+               }
+               
+               $this->embeddedContent = $metacodesByName;
+               
+               EventHandler::getInstance()->fireAction($this, 'parseEmbeddedContent');
        }
        
        protected function cleanup() {
diff --git a/wcfsetup/install/files/lib/system/html/input/node/IHtmlInputNodeProcessor.class.php b/wcfsetup/install/files/lib/system/html/input/node/IHtmlInputNodeProcessor.class.php
new file mode 100644 (file)
index 0000000..8911a8a
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+namespace wcf\system\html\input\node;
+use wcf\system\html\node\IHtmlNodeProcessor;
+
+/**
+ * @since 2.2
+ */
+interface IHtmlInputNodeProcessor extends IHtmlNodeProcessor {
+       public function getEmbeddedContent();
+       
+       public function process();
+}
index 72a34d8634ebc97dbefd8c4c19a12e53b8c82f47..c19abe20eb7c44d94ac89dd32f62c0a73ef3b6e6 100644 (file)
@@ -7,7 +7,7 @@ use wcf\util\JSON;
  * TOOD documentation
  * @since      2.2
  */
-class HtmlNodeProcessor {
+class HtmlNodeProcessor implements IHtmlNodeProcessor {
        /**
         * @var \DOMDocument
         */
diff --git a/wcfsetup/install/files/lib/system/html/node/IHtmlNodeProcessor.class.php b/wcfsetup/install/files/lib/system/html/node/IHtmlNodeProcessor.class.php
new file mode 100644 (file)
index 0000000..795a940
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+namespace wcf\system\html\node;
+
+/**
+ * @since 2.2
+ */
+interface IHtmlNodeProcessor {
+       public function getDocument();
+       
+       public function getHtml();
+       
+       public function load($html);
+}
diff --git a/wcfsetup/install/files/lib/system/message/MessageFormSettingsHandler.class.php b/wcfsetup/install/files/lib/system/message/MessageFormSettingsHandler.class.php
deleted file mode 100644 (file)
index 758da63..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-namespace wcf\system\message;
-use wcf\data\DatabaseObject;
-use wcf\system\WCF;
-
-/**
- * Provides utility functions for common tasks related to inline editing and quick reply.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    com.woltlab.wcf
- * @subpackage system.message
- * @category   Community Framework
- */
-class MessageFormSettingsHandler {
-       /**
-        * Computes the settings for BBCodes, Smilies and pre parsing. Optionally accepts the corresponding DatabaseObject
-        * whose values will be used in case the settings did not contain the individual values (legacy support).
-        * 
-        * @param       mixed[][]                       $parameters
-        * @param       \wcf\data\DatabaseObject        $object
-        * @param       string                          $permissionCanUseBBCodes
-        * @param       string                          $permissionCanUseSmilies
-        * @return      array
-        */
-       public static function getSettings(array $parameters, DatabaseObject $object = null, $permissionCanUseBBCodes = '', $permissionCanUseSmilies = '') {
-               $permissionCanUseBBCodes = ($permissionCanUseBBCodes) ?: 'user.message.canUseBBCodes';
-               $permissionCanUseSmilies = ($permissionCanUseSmilies) ?: 'user.message.canUseSmilies';
-               
-               $enableSmilies = 0;
-               $enableBBCodes = 0;
-               $preParse = 0;
-               
-               if (WCF::getSession()->getPermission($permissionCanUseSmilies)) {
-                       if (isset($parameters['enableSmilies'])) {
-                               $enableSmilies = ($parameters['enableSmilies']) ? 1 : 0;
-                       }
-                       else {
-                               $enableSmilies = ($object === null) ? 1 : $object->enableSmilies;
-                       }
-               }
-               else if ($object !== null) {
-                       $enableSmilies = ($object->enableSmilies) ? 1 : 0;
-               }
-               
-               if (WCF::getSession()->getPermission($permissionCanUseBBCodes)) {
-                       if (isset($parameters['enableBBCodes'])) {
-                               $enableBBCodes = ($parameters['enableBBCodes']) ? 1 : 0;
-                       }
-                       else {
-                               $enableBBCodes = ($object === null) ? 1 : $object->enableBBCodes;
-                       }
-                       
-                       if (isset($parameters['preParse'])) {
-                               $preParse = ($parameters['preParse'] && $enableBBCodes) ? 1 : 0;
-                       }
-                       else {
-                               $preParse = $enableBBCodes;
-                       }
-               }
-               else if ($object !== null) {
-                       $enableBBCodes = $preParse = ($object->enableBBCodes) ? 1 : 0;
-               }
-               
-               return [
-                       'enableSmilies' => $enableSmilies,
-                       'enableBBCodes' => $enableBBCodes,
-                       'preParse' => $preParse
-               ];
-       }
-}
index 972be6253ba39d0d7a7f2ac264a6ee09600ddab1..fb944814019f854822c730991781e6292d91dfad 100644 (file)
@@ -10,6 +10,7 @@ use wcf\system\event\EventHandler;
 use wcf\system\exception\ParentClassException;
 use wcf\system\exception\SystemException;
 use wcf\system\exception\UserInputException;
+use wcf\system\html\input\HtmlInputProcessor;
 use wcf\system\SingletonFactory;
 use wcf\system\WCF;
 use wcf\util\ArrayUtil;
@@ -145,8 +146,13 @@ class QuickReplyManager extends SingletonFactory {
                }
                $object->validateContainer($this->container);
                
+               $parameters['htmlInputProcessor'] = $object->getHtmlInputProcessor($parameters['data']['message']);
+               unset($parameters['data']['message']);
+               
+               $parameters['htmlInputProcessor']->validate();
+               
                // validate message
-               $object->validateMessage($this->container, $parameters['data']['message']);
+               $object->validateMessage($this->container, $parameters['htmlInputProcessor']);
                
                // check for message quote ids
                $parameters['removeQuoteIDs'] = (isset($parameters['removeQuoteIDs']) && is_array($parameters['removeQuoteIDs'])) ? ArrayUtil::trim($parameters['removeQuoteIDs']) : [];
@@ -158,12 +164,6 @@ class QuickReplyManager extends SingletonFactory {
                        unset($parameters['data']['tmpHash']);
                }
                
-               // message settings
-               $parameters['data'] = array_merge($parameters['data'], MessageFormSettingsHandler::getSettings($parameters));
-               
-               $parameters['data']['enableHtml'] = 0;
-               $parameters['data']['showSignature'] = (WCF::getUser()->userID ? WCF::getUser()->showSignature : 0);
-               
                EventHandler::getInstance()->fireAction($this, 'validateParameters', $parameters);
        }
        
@@ -171,7 +171,7 @@ class QuickReplyManager extends SingletonFactory {
         * Creates a new message and returns the parsed template.
         * 
         * @param       \wcf\data\IMessageQuickReplyAction      $object
-        * @param       mixed[][]                               $parameters
+        * @param       array                                   $parameters
         * @param       string                                  $containerActionClassName
         * @param       string                                  $sortOrder
         * @param       string                                  $templateName
@@ -189,10 +189,10 @@ class QuickReplyManager extends SingletonFactory {
                $parameters['data']['username'] = WCF::getUser()->username;
                
                // pre-parse message text
-               if ($parameters['data']['preParse']) {
+               /*if ($parameters['data']['preParse']) {
                        $parameters['data']['message'] = PreParser::getInstance()->parse($parameters['data']['message'], $this->allowedBBodes);
                }
-               unset($parameters['data']['preParse']);
+               unset($parameters['data']['preParse']);*/
                
                $parameters['data'] = array_merge($additionalFields, $parameters['data']);
                
@@ -206,9 +206,7 @@ class QuickReplyManager extends SingletonFactory {
                EventHandler::getInstance()->fireAction($this, 'createdMessage', $eventParameters);
                
                if ($message instanceof IMessage && !$message->isVisible()) {
-                       return [
-                               'isVisible' => false
-                       ];
+                       return ['isVisible' => false];
                }
                
                // resolve the page no
@@ -248,9 +246,7 @@ class QuickReplyManager extends SingletonFactory {
                }
                else {
                        // redirect
-                       return [
-                               'url' => $object->getRedirectUrl($this->container, $message)
-                       ];
+                       return ['url' => $object->getRedirectUrl($this->container, $message)];
                }
        }
        
index e8e4154921a7ac75cc99caa86792714bdfcdb59c..f387ab870b5e5961854270cf748209a49dbcc904 100644 (file)
@@ -32,6 +32,34 @@ class LangCompilerTemplatePlugin implements ICompilerTemplatePlugin {
         */
        public function executeEnd(TemplateScriptingCompiler $compiler) {
                $compiler->popTag('lang');
-               return "<?php echo (!empty(\$this->tagStack[count(\$this->tagStack) - 1][1]['__literal']) ? wcf\system\WCF::getLanguage()->get(ob_get_clean(), \$this->tagStack[count(\$this->tagStack) - 1][1], (isset(\$this->tagStack[count(\$this->tagStack) - 1][1]['__optional']) ? \$this->tagStack[count(\$this->tagStack) - 1][1]['__optional'] : false)) : wcf\system\WCF::getLanguage()->getDynamicVariable(ob_get_clean(), \$this->tagStack[count(\$this->tagStack) - 1][1], (isset(\$this->tagStack[count(\$this->tagStack) - 1][1]['__optional']) ? \$this->tagStack[count(\$this->tagStack) - 1][1]['__optional'] : false))); array_pop(\$this->tagStack); ?>";
+               return "<?php
+                       echo (
+                               !empty(\$this->tagStack[count(\$this->tagStack) - 1][1]['__literal'])
+                               ?
+                               wcf\system\WCF::getLanguage()->get(
+                                       ob_get_clean(),
+                                       \$this->tagStack[count(\$this->tagStack) - 1][1],
+                                       (
+                                               isset(\$this->tagStack[count(\$this->tagStack) - 1][1]['__optional'])
+                                               ?
+                                               \$this->tagStack[count(\$this->tagStack) - 1][1]['__optional']
+                                               :
+                                               false
+                                       )
+                               )
+                               :
+                               wcf\system\WCF::getLanguage()->getDynamicVariable(
+                                       ob_get_clean(),
+                                       \$this->tagStack[count(\$this->tagStack) - 1][1],
+                                       (
+                                               isset(\$this->tagStack[count(\$this->tagStack) - 1][1]['__optional'])
+                                               ?
+                                               \$this->tagStack[count(\$this->tagStack) - 1][1]['__optional']
+                                               :
+                                               false
+                                       )
+                               )
+                       );
+                       array_pop(\$this->tagStack); ?>";
        }
 }
index a92ffb2c45abb2e8f206a6a2c633000af20b7cd2..3ca32b7b3bb186acb44bd3b0d86c454e080c215c 100644 (file)
@@ -417,7 +417,12 @@ final class DOMUtil {
                $cloneNode = self::getParentBefore($node, $ancestor);
                
                if ($splitBefore) {
-                       if (self::isFirstNode($node, $cloneNode)) {
+                       if ($cloneNode === null) {
+                               // target node is already a direct descendant of the ancestor
+                               // node, no need to split anything
+                               return $node;
+                       }
+                       else if (self::isFirstNode($node, $cloneNode)) {
                                // target node is at the very start, we can safely move the
                                // entire parent node around
                                return $cloneNode;
@@ -436,7 +441,12 @@ final class DOMUtil {
                        }
                }
                else {
-                       if (self::isLastNode($node, $cloneNode)) {
+                       if ($cloneNode === null) {
+                               // target node is already a direct descendant of the ancestor
+                               // node, no need to split anything
+                               return $node;
+                       }
+                       else if (self::isLastNode($node, $cloneNode)) {
                                // target node is at the very end, we can safely move the
                                // entire parent node around
                                return $cloneNode;