From 95e10b89b97aba2631537a1858aeccbce07c6f44 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 26 Jan 2024 13:45:08 -0500 Subject: [PATCH] InputfieldTinyMCE: Add support for new "remove all style attributes" option to the Markup Toggle settings. Plus refactoring of the pasteFilter JS in attempt to fix processwire/processwire-issues#1866 which should improve pasting from MS Word. --- .../InputfieldTinyMCE/InputfieldTinyMCE.js | 41 +++++++++++----- .../InputfieldTinyMCE.module.php | 3 +- .../InputfieldTinyMCEConfigs.php | 3 +- .../InputfieldTinyMCETools.php | 48 ++++++++++++------- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.js b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.js index 55616434..89ccd359 100644 --- a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.js +++ b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.js @@ -598,7 +598,7 @@ var InputfieldTinyMCE = { var t = InputfieldTinyMCE; var allow = ',' + ProcessWire.config.InputfieldTinyMCE.pasteFilter + ','; - var regexTag = /<([a-z0-9]+)([^>]*)>/gi; + var regexTag = /<([a-z0-9:!\[\]]+)([^>]*)>/gi; var regexAttr = /([-_a-z0-9]+)=["']([^"']*)["']/gi; var html = args.content; var matchTag, matchAttr; @@ -607,13 +607,30 @@ var InputfieldTinyMCE = { var replaces = []; var startLength = html.length; + allow = allow.toLowerCase(); + + /* + * HTML for testing MS word paste + msWordHtml = + "

This is bold text.

\n\n" + + "

This is headline 2.

\n\n" + + "

This is italic text

\n\n" + + "

" + + "" + + "1." + + "     One

"; + html = msWordHtml; + */ + + startLength = html.length; + if(args.internal) { t.log('Skipping pasteFilter for interal copy/paste'); return; // skip filtering for internal copy/paste operations } if(allow === ',text,') { - t.log('Skipping pasteFilter since paste_as_text settingw will be used'); + t.log('Skipping pasteFilter since paste_as_text settings will be used'); return; // will be processed by paste_as_text setting } @@ -621,17 +638,19 @@ var InputfieldTinyMCE = { var tagOpen = matchTag[0]; // i.e. , ,

, etc. var tagName = matchTag[1]; // i.e. 'strong', 'img', 'h2', etc. + var tagNameLower = tagName.toLowerCase(); var tagClose = ''; // i.e. ,

var tagAttrs = matchTag[2]; // i.e. 'src="a.jpg" alt="alt"' var allowAttrs = false; // first see if we can match a tag replacement - var find = ',' + tagName + '='; // i.e. ',b=strong' - var pos = allow.indexOf(find); + var findTagEqual = ',' + tagNameLower + '='; // i.e. ',b=strong' + var findTagAttr = ',' + tagNameLower + '['; // i.e. ',b[...]' + var findTagOnly = ',' + tagNameLower + ','; // i.e. ,b + var posTagEqual = allow.indexOf(findTagEqual); - if(pos > -1) { - // tag replacement - var rule = allow.substring(pos + 1); // i.e. b=strong,and,more + if(posTagEqual > -1) { + var rule = allow.substring(posTagEqual + 1); // i.e. b=strong,and,more rule = rule.substring(0, rule.indexOf(',')); // i.e. b=strong rule = rule.split('='); var replaceTag = rule[1]; @@ -640,12 +659,12 @@ var InputfieldTinyMCE = { finds.push(tagClose); replaces.push(''); } - - if(allow.indexOf(',' + tagName + '[') > -1) { + + if(allow.indexOf(findTagAttr) > -1) { // tag appears in whitelist with attributes allowAttrs = true; - } else if(allow.indexOf(',' + tagName + ',') === -1) { - // tag does not appear in whitelist + } else if(posTagEqual === -1 && allow.indexOf(findTagOnly) === -1) { + // tag does not appear in whitelist at all removals.push(tagOpen); removals.push(tagClose); continue; diff --git a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.module.php b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.module.php index e0bee23a..ffbfa2d4 100644 --- a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.module.php +++ b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCE.module.php @@ -74,7 +74,7 @@ class InputfieldTinyMCE extends InputfieldTextarea implements ConfigurableModule return array( 'title' => 'TinyMCE', 'summary' => 'TinyMCE rich text editor version ' . self::mceVersion . '.', - 'version' => 617, + 'version' => 618, 'icon' => 'keyboard-o', 'requires' => 'ProcessWire>=3.0.200, MarkupHTMLPurifier', ); @@ -89,6 +89,7 @@ class InputfieldTinyMCE extends InputfieldTextarea implements ConfigurableModule const toggleCleanDiv = 2; // remove
s const toggleCleanP = 4; // remove empty

tags const toggleCleanNbsp = 8; // remove   entities + const toggleRemoveStyles = 16; // remove all style attributes /** * Default configuration for filtered paste diff --git a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCEConfigs.php b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCEConfigs.php index fee78bc0..d2f0ef5f 100644 --- a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCEConfigs.php +++ b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCEConfigs.php @@ -711,6 +711,7 @@ class InputfieldTinyMCEConfigs extends InputfieldTinyMCEClass { $f->addOption(InputfieldTinyMCE::toggleCleanDiv, $this->_('Convert `

` tags to `

` tags on save?')); $f->addOption(InputfieldTinyMCE::toggleCleanP, $this->_('Remove empty `

` tags on save?')); $f->addOption(InputfieldTinyMCE::toggleCleanNbsp, $this->_('Remove non-breaking spaces on save?')); + $f->addOption(InputfieldTinyMCE::toggleRemoveStyles, $this->_('Remove `style` attributes from all elements')); $f->attr('value', $this->inputfield->toggles); $f->collapsed = Inputfield::collapsedYes; $f->themeOffset = 1; @@ -1385,4 +1386,4 @@ class InputfieldTinyMCEConfigs extends InputfieldTinyMCEClass { 'usePurifier', ); -} \ No newline at end of file +} diff --git a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCETools.php b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCETools.php index 79495dd7..41b1b258 100644 --- a/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCETools.php +++ b/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCETools.php @@ -162,25 +162,41 @@ class InputfieldTinyMCETools extends InputfieldTinyMCEClass { // convert

to paragraphs $toggles = $this->inputfield->toggles; if(!is_array($toggles)) return $value; - - if(in_array(InputfieldTinyMCE::toggleCleanDiv, $toggles) && strpos($value, '<]*>\s*}is', '$1' . 'p>', $value); - while(strpos($value, '

') !== false) { - $value = str_replace(array('

', '

'), array('

', '

'), $value); + + foreach($toggles as $toggle) { + switch($toggle) { + case InputfieldTinyMCE::toggleCleanDiv: + // convert
to

+ if(strpos($value, '<]*>\s*}is', '$1' . 'p>', $value); + while(strpos($value, '

') !== false) { + $value = str_replace(array('

', '

'), array('

', '

'), $value); + } + } + break; + case InputfieldTinyMCE::toggleCleanP: + // remove empty paragraphs + $value = str_replace(array('


', '

 

', "

\xc2\xa0

", '

', '

'), '', $value); + break; + case InputfieldTinyMCE::toggleCleanNbsp: + // convert non-breaking space to regular space + $value = str_ireplace(' ', ' ', $value); + $value = str_replace("\xc2\xa0",' ', $value); + break; + case InputfieldTinyMCE::toggleRemoveStyles: + // remove all style attributes + if(strpos($value, 'style=')) { + if(preg_match_all('!(<.+?)\sstyle=(["\']).*?\2!i', $value, $matches)) { + foreach($matches[0] as $key => $fullMatch) { + $startMatch = $matches[1][$key]; + $value = str_replace($fullMatch, $startMatch, $value); + } + } + } + break; } } - // remove gratuitous whitespace - if(in_array(InputfieldTinyMCE::toggleCleanP, $toggles)) { - $value = str_replace(array('


', '

 

', "

\xc2\xa0

", '

', '

'), '', $value); - } - - // convert non-breaking space to regular space - if(in_array(InputfieldTinyMCE::toggleCleanNbsp, $toggles)) { - $value = str_ireplace(' ', ' ', $value); - $value = str_replace("\xc2\xa0",' ', $value); - } - return $value; }