From 8b10b2accf00013ccd5d4e8f00897fb81f7093a4 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sun, 27 Dec 2015 23:29:04 +0100 Subject: [PATCH] Improved App.Utils class. --- .../javascripts/app/lib/app_post/utils.coffee | 92 +++++++++++-------- .../app/lib/base/jquery.contenteditable.js | 51 ++++++---- .../javascripts/app/lib/base/word_filter.js | 84 +++++++++++++++++ public/assets/tests/html-utils.js | 52 ++++++++++- 4 files changed, 217 insertions(+), 62 deletions(-) create mode 100644 app/assets/javascripts/app/lib/base/word_filter.js diff --git a/app/assets/javascripts/app/lib/app_post/utils.coffee b/app/assets/javascripts/app/lib/app_post/utils.coffee index 01520a2ad..a31859c52 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.coffee @@ -1,24 +1,24 @@ # coffeelint: disable=no_unnecessary_double_quotes class App.Utils - # textCleand = App.Utils.textCleanup( rawText ) - @textCleanup: ( ascii ) -> + # textCleand = App.Utils.textCleanup(rawText) + @textCleanup: (ascii) -> $.trim( ascii ) .replace(/(\r\n|\n\r)/g, "\n") # cleanup .replace(/\r/g, "\n") # cleanup .replace(/[ ]\n/g, "\n") # remove tailing spaces .replace(/\n{3,20}/g, "\n\n") # remove multiple empty lines - # htmlEscapedAndLinkified = App.Utils.text2html( rawText ) - @text2html: ( ascii ) -> + # htmlEscapedAndLinkified = App.Utils.text2html(rawText) + @text2html: (ascii) -> ascii = @textCleanup(ascii) #ascii = @htmlEscape(ascii) ascii = @linkify(ascii) ascii = '
' + ascii.replace(/\n/g, '
') + '
' ascii.replace(/
<\/div>/g, '

') - # rawText = App.Utils.html2text( html ) - @html2text: ( html ) -> + # rawText = App.Utils.html2text(html) + @html2text: (html) -> # remove not needed new lines html = html.replace(/>\n/g, '>') @@ -37,11 +37,11 @@ class App.Utils .replace(/\r/g, "\n") # cleanup .replace(/\n{3,20}/g, "\n\n") # remove multiple empty lines - # htmlEscapedAndLinkified = App.Utils.linkify( rawText ) + # htmlEscapedAndLinkified = App.Utils.linkify(rawText) @linkify: (ascii) -> - window.linkify( ascii ) + window.linkify(ascii) - # wrappedText = App.Utils.wrap( rawText, maxLineLength ) + # wrappedText = App.Utils.wrap(rawText, maxLineLength) @wrap: (ascii, max = 82) -> result = '' counter_lines = 0 @@ -80,19 +80,19 @@ class App.Utils result += "\n" result - # quotedText = App.Utils.quote( rawText ) + # quotedText = App.Utils.quote(rawText) @quote: (ascii, max = 82) -> ascii = @textCleanup(ascii) ascii = @wrap(ascii, max) - $.trim( ascii ) + $.trim(ascii) .replace /^(.*)$/mg, (match) -> if match '> ' + match else '>' - # htmlEscaped = App.Utils.htmlEscape( rawText ) - @htmlEscape: ( ascii ) -> + # htmlEscaped = App.Utils.htmlEscape(rawText) + @htmlEscape: (ascii) -> return ascii if !ascii return ascii if !ascii.replace ascii.replace(/&/g, '&') @@ -101,8 +101,9 @@ class App.Utils .replace(/"/g, '"') .replace(/'/g, ''') - # textWithoutTags = App.Utils.htmlRemoveTags( html ) + # textWithoutTags = App.Utils.htmlRemoveTags(html) @htmlRemoveTags: (html) -> + html = @_checkTypeOf(html) # remove comments @_removeComments(html) @@ -111,17 +112,18 @@ class App.Utils @_removeWordMarkup(html) # remove tags, keep content - html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6').replaceWith( -> + html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6').replaceWith( -> $(@).contents() ) # remove tags & content - html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, textarea, font, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6, br, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe').remove() + html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6, br, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head').remove() html - # htmlOnlyWithRichtext = App.Utils.htmlRemoveRichtext( html ) + # htmlOnlyWithRichtext = App.Utils.htmlRemoveRichtext(html) @htmlRemoveRichtext: (html) -> + html = @_checkTypeOf(html) # remove comments @_removeComments(html) @@ -133,17 +135,18 @@ class App.Utils @_removeWordMarkup(html) # remove tags, keep content - html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6').replaceWith( -> + html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6').replaceWith( -> $(@).contents() ) # remove tags & content - html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe').remove() + html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head').remove() html - # cleanHtmlWithRichText = App.Utils.htmlCleanup( html ) + # cleanHtmlWithRichText = App.Utils.htmlCleanup(html) @htmlCleanup: (html) -> + html = @_checkTypeOf(html) # remove comments @_removeComments(html) @@ -155,7 +158,7 @@ class App.Utils @_removeWordMarkup(html) # remove tags, keep content - html.find('a, font, small, time, form').replaceWith( -> + html.find('a, font, small, time, form, label').replaceWith( -> $(@).contents() ) @@ -179,16 +182,22 @@ class App.Utils ) # remove tags & content - html.find('font, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe').remove() + html.find('font, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove() html + @_checkTypeOf: (item) -> + return item if typeof item isnt 'string' + return $(item) if item.substr(0,9) isnt '#{item}
") + @_removeAttributes: (html) -> html.find('*') - .removeAttr( 'style' ) - .removeAttr( 'class' ) - .removeAttr( 'title' ) - .removeAttr( 'lang' ) + .removeAttr('style') + .removeAttr('class') + .removeAttr('title') + .removeAttr('lang') + .removeAttr('type') html @_removeComments: (html) -> @@ -201,37 +210,38 @@ class App.Utils @_removeWordMarkup: (html) -> match = false htmlTmp = html.get(0).outerHTML - regex = new RegExp('<(/w|w)\:[A-Za-z]{3}>') + regex = new RegExp('<(/w|w)\:[A-Za-z]') if htmlTmp.match(regex) match = true htmlTmp = htmlTmp.replace(regex, '') - regex = new RegExp('<(/o|o)\:[A-Za-z]{1}>') + regex = new RegExp('<(/o|o)\:[A-Za-z]') if htmlTmp.match(regex) match = true htmlTmp = htmlTmp.replace(regex, '') if match - html.html(htmlTmp) + return window.word_filter(html) + html - # signatureNeeded = App.Utils.signatureCheck( message, signature ) + # signatureNeeded = App.Utils.signatureCheck(message, signature) @signatureCheck: (message, signature) -> - messageText = $( '
' + message + '
' ).text().trim() + messageText = $('
' + message + '
').text().trim() messageText = messageText.replace(/(\n|\r|\t)/g, '') - signatureText = $( '
' + signature + '
' ).text().trim() + signatureText = $('
' + signature + '
').text().trim() signatureText = signatureText.replace(/(\n|\r|\t)/g, '') quote = (str) -> (str + '').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&") #console.log('SC', messageText, signatureText, quote(signatureText)) - regex = new RegExp( quote(signatureText), 'mi' ) + regex = new RegExp(quote(signatureText), 'mi') if messageText.match(regex) false else true - # messageWithMarker = App.Utils.signatureIdentify( message, false ) + # messageWithMarker = App.Utils.signatureIdentify(message, false) @signatureIdentify: (message, test = false) -> - textToSearch = @html2text( message ) + textToSearch = @html2text(message) # count lines, if we do have lower the 10, ignore this textToSearchInLines = textToSearch.split("\n") @@ -450,19 +460,23 @@ class App.Utils value ) - # true|false = App.Utils.lastLineEmpty( message ) + # true|false = App.Utils.lastLineEmpty(message) @lastLineEmpty: (message) -> messageCleanup = message.replace(/>\s+<').replace(/(\n|\r|\t)/g, '').trim() return true if messageCleanup.match(/<(br|\s+?|\/)>$/im) return true if messageCleanup.match(/<\/div>$/im) false - # cleanString = App.Utils.htmlAttributeCleanup( string ) + # string = App.Utils.removeEmptyLines(stringWithEmptyLines) + @removeEmptyLines: (string) -> + string.replace(/^\s*[\r\n]/gm, '') + + # cleanString = App.Utils.htmlAttributeCleanup(string) @htmlAttributeCleanup: (string) -> string.replace(/((?![-a-zA-Z0-9_]+).|\n|\r|\t)/gm, '') - # diff = App.Utils.formDiff( dataNow, dataLast ) - @formDiff: ( dataNowRaw, dataLastRaw ) -> + # diff = App.Utils.formDiff(dataNow, dataLast) + @formDiff: (dataNowRaw, dataLastRaw) -> dataNow = clone(dataNowRaw) @_formDiffNormalizer(dataNow) dataLast = clone(dataLastRaw) diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js index 359520f83..85896dfda 100644 --- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js +++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js @@ -96,41 +96,54 @@ // just paste text this.$element.on('paste', function (e) { + e.preventDefault() _this.log('paste') // check existing + paste text for limit - var text - if (window.clipboardData) { // IE - text = window.clipboardData.getData('Text') + var text = e.originalEvent.clipboardData.getData('text/html') + var docType = 'html' + if (!text || text.length === 0) { + docType = 'text' + text = e.originalEvent.clipboardData.getData('text/plain') } - else { - text = (e.originalEvent || e).clipboardData.getData('text/plain') + if (!text || text.length === 0) { + docType = 'text2' + text = e.originalEvent.clipboardData.getData('text') } + _this.log('paste', docType, text) - if ( !_this.maxLengthOk( text.length) ) { - e.preventDefault() + if (!_this.maxLengthOk(text.length)) { return } - // use setTimeout() with 0 to execute it right after paste event - if ( _this.options.mode === 'textonly' ) { - if ( !_this.options.multiline ) { - setTimeout($.proxy(function(){ - App.Utils.htmlRemoveTags(this.$element) - }, _this), 0) + if (docType == 'html') { + text = '
' + text + '
' // to prevent multible dom object. we need it at level 0 + if (_this.options.mode === 'textonly') { + if (!_this.options.multiline) { + text = App.Utils.htmlRemoveTags(text) + } + else { + text = App.Utils.htmlRemoveRichtext(text) + } } else { - setTimeout($.proxy(function(){ - App.Utils.htmlRemoveRichtext(this.$element) - }, _this), 0) + text = App.Utils.htmlCleanup(text) + } + text = text.html() + + // as fallback, take text + if (!text) { + text = App.Utils.text2html(text.text()) } } else { - setTimeout($.proxy(function(){ - App.Utils.htmlCleanup(this.$element) - }, _this), 0) + text = App.Utils.text2html(text) } + // cleanup + text = App.Utils.removeEmptyLines(text) + _this.log('insert', test) + document.execCommand('insertHTML', false, text) return true }) diff --git a/app/assets/javascripts/app/lib/base/word_filter.js b/app/assets/javascripts/app/lib/base/word_filter.js new file mode 100644 index 000000000..9300d07f9 --- /dev/null +++ b/app/assets/javascripts/app/lib/base/word_filter.js @@ -0,0 +1,84 @@ +// (C) sbrin - https://github.com/sbrin +// https://gist.github.com/sbrin/6801034 +window.word_filter = function(editor){ + var content = editor.html(); + + // Word comments like conditional comments etc + content = content.replace(//gi, ''); + + // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content, + // MS Office namespaced tags, and a few other tags + content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, ''); + + // Convert into for line-though + content = content.replace(/<(\/?)s>/gi, "<$1strike>"); + + // Replace nbsp entites to char since it's easier to handle + //content = content.replace(/ /gi, "\u00a0"); + content = content.replace(/ /gi, ' '); + + // Convert ___ to string of alternating + // breaking/non-breaking spaces of same length + content = content.replace(/([\s\u00a0]*)<\/span>/gi, function(str, spaces) { + return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''; + }); + + editor.html(content); + + // Parse out list indent level for lists + $('p', editor).each(function(){ + var str = $(this).attr('style'); + var matches = /mso-list:\w+ \w+([0-9]+)/.exec(str); + if (matches) { + $(this).data('_listLevel', parseInt(matches[1], 10)); + } + }); + + // Parse Lists + var last_level=0; + var pnt = null; + $('p', editor).each(function(){ + var cur_level = $(this).data('_listLevel'); + if(cur_level != undefined){ + var txt = $(this).text(); + var list_tag = '
    '; + if (/^\s*\w+\./.test(txt)) { + var matches = /([0-9])\./.exec(txt); + if (matches) { + var start = parseInt(matches[1], 10); + list_tag = start>1 ? '
      ' : '
        '; + }else{ + list_tag = '
          '; + } + } + + if(cur_level>last_level){ + if(last_level==0){ + $(this).before(list_tag); + pnt = $(this).prev(); + }else{ + pnt = $(list_tag).appendTo(pnt); + } + } + if(cur_level' + $(this).html() + '') + $(this).remove(); + last_level = cur_level; + }else{ + last_level = 0; + } + }) + + $('[style]', editor).removeAttr('style'); + $('[align]', editor).removeAttr('align'); + $('span', editor).replaceWith(function() {return $(this).contents();}); + $('span:empty', editor).remove(); + $("[class^='Mso']", editor).removeAttr('class'); + $('p:empty', editor).remove(); + return editor +} diff --git a/public/assets/tests/html-utils.js b/public/assets/tests/html-utils.js index 385ddcde7..2a71d2ac4 100644 --- a/public/assets/tests/html-utils.js +++ b/public/assets/tests/html-utils.js @@ -143,7 +143,6 @@ test("linkify", function() { result = App.Utils.linkify(source) equal(result, should, source) - /* source = "example.com" should = 'http://example.com' @@ -263,6 +262,9 @@ test("htmlRemoveTags", function() { should = "This is some text!" result = App.Utils.htmlRemoveRichtext($(source)) equal(result.html(), should, source) + result = App.Utils.htmlRemoveRichtext(source) + equal(result.html(), should, source) + }); // htmlRemoveRichtext @@ -277,6 +279,7 @@ test("htmlRemoveRichtext", function() { source = "
          1.1.1     Description
          " //should = "
          1.1.1     Description
          " should = "1.1.1     Description" + //should = '1.1.1 Description' result = App.Utils.htmlRemoveRichtext($(source)) equal(result.html(), should, source) @@ -312,6 +315,7 @@ test("htmlRemoveRichtext", function() { source = "
          test
          123
          " //should = "
          test
          123
          " should = "
          test
          123" + //should = '
          test
          123' result = App.Utils.htmlRemoveRichtext($(source)) equal(result.html(), should, source) @@ -345,12 +349,21 @@ test("htmlRemoveRichtext", function() { result = App.Utils.htmlRemoveRichtext($(source)) equal(result.html(), should, source) + source = "
          \n" + //should = "
          test 123
          " + should = '
          Gruppe *
          Besitzer
          Status *
          ' + result = App.Utils.htmlRemoveRichtext(source) + equal(result.html(), should, source) + source = "
          This is some text!
          " //should = "
          This is some text!
          " should = "This is some text!" result = App.Utils.htmlRemoveRichtext($(source)) equal(result.html(), should, source) + result = App.Utils.htmlRemoveRichtext(source) + equal(result.html(), should, source) + }); // htmlCleanup @@ -388,6 +401,7 @@ test("htmlCleanup", function() { source = "

          some h1 for somewhere


          " //should = "
          some h1 for somewhere

          " should = "
          some h1 for somewhere

          " + //should = '
          some h1 for somewhere
          ' result = App.Utils.htmlCleanup($(source)) equal(result.html(), should, source) @@ -428,10 +442,27 @@ test("htmlCleanup", function() { equal(result.html(), should, source) source = "

          some link to somewhere from wordabc

          " - should = "

          some link to somewhere from wordabc

          " + //should = "

          some link to somewhere from wordabc

          " + should = '

          some link to somewhere from wordabc

          ' result = App.Utils.htmlCleanup($(source)) equal(result.html(), should, source) + source = "
          \n" + //should = "
          test 123
          " + should = '
          Gruppe *
          Besitzer
          Status *
          ' + result = App.Utils.htmlCleanup(source) + equal(result.html(), should, source) + + source = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n

          ·            \nTest 1

          \n\n

          ·            \nTest 2

          \n\n

          ·            \nTest 3

          \n\n

          ·            \nTest 4

          \n\n

          ·            \nTest5

          \n\n\n\n\n" + should = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n

          · \nTest 1

          \n\n

          · \nTest 2

          \n\n

          · \nTest 3

          \n\n

          · \nTest 4

          \n\n

          · \nTest5

          \n\n\n\n\n" + result = App.Utils.htmlCleanup(source) + equal(result.html(), should, source) + + source = "\n\n\n \n \n \n \n\n\n

          1.\nGehe auf https://www.pferdiathek.ge

          \n


          \n\n

          \n

          2.\nMelde Dich mit folgende Zugangsdaten an:

          \n

          Benutzer:\nme@xxx.net

          \n

          Passwort:\nxxx.

          \n\n" + should = "\n\n\n \n \n \n \n\n\n

          1.\nGehe auf https://www.pferdiathek.ge

          \n


          \n\n

          \n

          2.\nMelde Dich mit folgende Zugangsdaten an:

          \n

          Benutzer:\nme@xxx.net

          \n

          Passwort:\nxxx.

          \n\n" + result = App.Utils.htmlCleanup(source) + equal(result.html(), should, source) + }); // wrap @@ -469,6 +500,21 @@ test("wrap", function() { }); +// remove empty lines +test("remove empty lines", function() { + + var source = "\ntest 123\n" + var should = "test 123\n" + var result = App.Utils.removeEmptyLines(source) + equal(result, should, source) + + source = "\ntest\n\n123\n" + should = "test\n123\n" + result = App.Utils.removeEmptyLines(source) + equal(result, should, source) + +}); + // quote test("quote", function() { @@ -492,13 +538,11 @@ test("quote", function() { result = App.Utils.quote(source) equal(result, should, source) - source = "Welcome! Thank you for installing Zammad. You will find ..." should = "> Welcome! Thank you\n> for installing\n> Zammad. You will\n> find ..." result = App.Utils.quote(source, 20) equal(result, should, source) - }); // check signature