feat(paste): Paste accepts more HTML, paste and match style now available

Summary:
Related to #320, #494, #515, #553

Ignore newlines and returns in HTML, they can be inside tags

Allow all attributes so that paste from excel looks nice

Never let someone paste a `contenteditable` attribute

Update specs

Test Plan: Run new specs

Reviewers: juan, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2309
This commit is contained in:
Ben Gotow 2015-12-07 15:34:03 -08:00
parent d8c79a4d50
commit a1bb98ebf4
34 changed files with 3518 additions and 328 deletions

View file

@ -9,7 +9,7 @@ request = require 'request'
{React,
ComponentRegistry,
QuotedHTMLParser,
QuotedHTMLTransformer,
DraftStore} = require 'nylas-exports'
{Menu,
RetinaImg,
@ -83,7 +83,7 @@ class TranslateButton extends React.Component
#
session = DraftStore.sessionForClientId(@props.draftClientId).then (session) =>
draftHtml = session.draft().body
text = QuotedHTMLParser.removeQuotedHTML(draftHtml)
text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml)
query =
key: YandexTranslationKey
@ -99,7 +99,7 @@ class TranslateButton extends React.Component
# The new text of the draft is our translated response, plus any quoted text
# that we didn't process.
translated = json.text.join('')
translated = QuotedHTMLParser.appendQuotedHTML(translated, draftHtml)
translated = QuotedHTMLTransformer.appendQuotedHTML(translated, draftHtml)
# To update the draft, we add the new body to it's session. The session object
# automatically marshalls changes to the database and ensures that others accessing

View file

@ -60,7 +60,7 @@ your availabilities to schedule an appointment with you.</p>
{ComponentRegistry,
DatabaseStore,
DraftStore,
QuotedHTMLParser,
QuotedHTMLTransformer,
Event} = <span class="hljs-built_in">require</span> <span class="hljs-string">'nylas-exports'</span>
url = <span class="hljs-built_in">require</span>(<span class="hljs-string">'url'</span>)
@ -315,8 +315,7 @@ subscribing to events, release them here.</p>
<div class="content"><div class='highlight'><pre> DraftStore.sessionForClientId(draftClientId).<span class="hljs-keyword">then</span> (session) =&gt;
draftHtml = session.draft().body
text = QuotedHTMLParser.removeQuotedHTML(draftHtml)</pre></div></div>
text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml)</pre></div></div>
</li>
@ -333,8 +332,7 @@ subscribing to events, release them here.</p>
<div class="content"><div class='highlight'><pre> <span class="hljs-built_in">console</span>.log(<span class="hljs-property">@_createBlock</span>(events,eventData));
text += <span class="hljs-string">"&lt;br/&gt;"</span>+<span class="hljs-property">@_createBlock</span>(events,eventData)+<span class="hljs-string">"&lt;br/&gt;"</span>;
newDraftHtml = QuotedHTMLParser.appendQuotedHTML(text, draftHtml)</pre></div></div>
newDraftHtml = QuotedHTMLTransformer.appendQuotedHTML(text, draftHtml)</pre></div></div>
</li>

View file

@ -7,7 +7,7 @@
{ComponentRegistry,
DatabaseStore,
DraftStore,
QuotedHTMLParser,
QuotedHTMLTransformer,
ExtensionRegistry,
Event} = require 'nylas-exports'
@ -131,12 +131,12 @@ module.exports =
# Obtain the session for the current draft.
DraftStore.sessionForClientId(draftClientId).then (session) =>
draftHtml = session.draft().body
text = QuotedHTMLParser.removeQuotedHTML(draftHtml)
text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml)
# add the block
text += "<br/>"+@_createBlock(events,eventData)+"<br/>"
newDraftHtml = QuotedHTMLParser.appendQuotedHTML(text, draftHtml)
newDraftHtml = QuotedHTMLTransformer.appendQuotedHTML(text, draftHtml)
# update the draft
session.changes.add(body: newDraftHtml)

View file

@ -9,7 +9,7 @@ React = require 'react'
ContactStore,
AccountStore,
FileUploadStore,
QuotedHTMLParser,
QuotedHTMLTransformer,
FileDownloadStore,
ExtensionRegistry} = require 'nylas-exports'
@ -327,14 +327,14 @@ class ComposerView extends React.Component
_removeQuotedText: (html) =>
if @state.showQuotedText then return html
else return QuotedHTMLParser.removeQuotedHTML(html)
else return QuotedHTMLTransformer.removeQuotedHTML(html)
_showQuotedText: (html) =>
if @state.showQuotedText then return html
else return QuotedHTMLParser.appendQuotedHTML(html, @state.body)
else return QuotedHTMLTransformer.appendQuotedHTML(html, @state.body)
_renderQuotedTextControl: ->
if QuotedHTMLParser.hasQuotedHTML(@state.body)
if QuotedHTMLTransformer.hasQuotedHTML(@state.body)
text = if @state.showQuotedText then "Hide" else "Show"
<a className="quoted-text-control" onClick={@_onToggleQuotedText}>
<span className="dots">&bull;&bull;&bull;</span>{text} previous
@ -712,7 +712,7 @@ class ComposerView extends React.Component
Actions.sendDraft(@props.draftClientId)
_mentionsAttachment: (body) =>
body = QuotedHTMLParser.removeQuotedHTML(body.toLowerCase().trim())
body = QuotedHTMLTransformer.removeQuotedHTML(body.toLowerCase().trim())
return body.indexOf("attach") >= 0
_destroyDraft: =>

View file

@ -122,7 +122,7 @@ body.platform-win32 {
padding-top: 12px;
}
input, textarea, .composer-participant-field {
input, textarea {
color: @text-color;
position: relative;
display: block;
@ -313,6 +313,7 @@ body.platform-win32 {
flex-shrink: 0;
border-bottom: 1px solid @border-color-divider;
min-height: 49px;
color: @text-color;
.button-dropdown {
margin-left: 10px;

View file

@ -1,7 +1,7 @@
React = require 'react'
_ = require "underscore"
{EventedIFrame} = require 'nylas-component-kit'
{QuotedHTMLParser} = require 'nylas-exports'
{QuotedHTMLTransformer} = require 'nylas-exports'
EmailFrameStylesStore = require './email-frame-styles-store'
@ -82,7 +82,7 @@ class EmailFrame extends React.Component
if @props.showQuotedText
@props.content
else
QuotedHTMLParser.removeQuotedHTML(@props.content, keepIfWholeBodyIsQuote: true)
QuotedHTMLTransformer.removeQuotedHTML(@props.content, keepIfWholeBodyIsQuote: true)
module.exports = EmailFrame

View file

@ -4,7 +4,7 @@ EmailFrame = require './email-frame'
{Utils,
MessageUtils,
MessageBodyProcessor,
QuotedHTMLParser,
QuotedHTMLTransformer,
FileDownloadStore} = require 'nylas-exports'
{InjectedComponentSet} = require 'nylas-component-kit'
@ -47,7 +47,7 @@ class MessageItemBody extends React.Component
<EmailFrame showQuotedText={@state.showQuotedText} content={@state.processedBody}/>
_renderQuotedTextControl: =>
return null unless QuotedHTMLParser.hasQuotedHTML(@props.message.body)
return null unless QuotedHTMLTransformer.hasQuotedHTML(@props.message.body)
text = if @state.showQuotedText then "Hide" else "Show"
<a className="quoted-text-control" onClick={@_toggleQuotedText}>
<span className="dots">&bull;&bull;&bull;</span>{text} previous

View file

@ -12,7 +12,7 @@ MessageControls = require './message-controls'
AccountStore,
MessageStore,
MessageBodyProcessor,
QuotedHTMLParser,
QuotedHTMLTransformer,
ComponentRegistry,
FileDownloadStore} = require 'nylas-exports'
{RetinaImg,

View file

@ -7,7 +7,7 @@ ReactTestUtils = React.addons.TestUtils
File,
Thread,
Utils,
QuotedHTMLParser,
QuotedHTMLTransformer,
FileDownloadStore,
MessageBodyProcessor} = require "nylas-exports"

View file

@ -27,6 +27,7 @@
'cmdctrl-x': 'core:cut'
'cmdctrl-c': 'core:copy'
'cmdctrl-v': 'core:paste'
'cmdctrl-shift-v': 'core:paste-and-match-style'
'cmdctrl-a': 'core:select-all'
'up' : 'core:previous-item'

View file

@ -42,6 +42,7 @@
{ label: 'Cut', command: 'core:cut' }
{ label: 'Copy', command: 'core:copy' }
{ label: 'Paste', command: 'core:paste' }
{ label: 'Paste and Match Style', command: 'core:paste-and-match-style' }
{ label: 'Select All', command: 'core:select-all' }
]
}

View file

@ -22,6 +22,7 @@
{ label: '&Cut', command: 'core:cut' }
{ label: 'C&opy', command: 'core:copy' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Paste and Match Style', command: 'core:paste-and-match-style' }
{ label: 'Select &All', command: 'core:select-all' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }

View file

@ -9,6 +9,7 @@
{ label: 'Cu&t', command: 'core:cut' }
{ label: '&Copy', command: 'core:copy' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Paste and Match Style', command: 'core:paste-and-match-style' }
{ label: 'Select &All', command: 'core:select-all' }
]
}

View file

@ -1,18 +1,21 @@
ClipboardService = require '../../src/components/contenteditable/clipboard-service'
{InlineStyleTransformer, SanitizeTransformer} = require 'nylas-exports'
fs = require 'fs'
describe "ClipboardService", ->
beforeEach ->
@onFilePaste = jasmine.createSpy('onFilePaste')
@clipboardService = new ClipboardService
spyOn(document, 'execCommand')
describe "when html and plain text parts are present", ->
describe "when both html and plain text parts are present", ->
beforeEach ->
@mockEvent =
preventDefault: jasmine.createSpy('preventDefault')
clipboardData:
getData: ->
return '<strong>This is text</strong>' if 'text/html'
return 'This is plain text' if 'text/plain'
getData: (mimetype) ->
return '<strong>This is text</strong>' if mimetype is 'text/html'
return 'This is plain text' if mimetype is 'text/plain'
return null
items: [{
kind: 'string'
@ -24,144 +27,95 @@ describe "ClipboardService", ->
getAsString: -> 'This is plain text'
}]
it "should sanitize the HTML string and call insertHTML", ->
spyOn(document, 'execCommand')
spyOn(@clipboardService, '_sanitizeInput').andCallThrough()
it "should choose to insert the HTML representation", ->
spyOn(@clipboardService, '_sanitizeHTMLInput').andCallFake (input) =>
Promise.resolve(input)
runs ->
@clipboardService.onPaste(@mockEvent)
waitsFor ->
document.execCommand.callCount > 0
runs ->
expect(@clipboardService._sanitizeInput).toHaveBeenCalledWith('<strong>This is text</strong>', 'text/html')
[command, a, html] = document.execCommand.mostRecentCall.args
expect(command).toEqual('insertHTML')
expect(html).toEqual('<strong>This is text</strong>')
describe "when html and plain text parts are present", ->
describe "when only plain text is present", ->
beforeEach ->
@mockEvent =
preventDefault: jasmine.createSpy('preventDefault')
clipboardData:
getData: ->
return 'This is plain text' if 'text/plain'
getData: (mimetype) ->
return 'This is plain text\nAnother line Hello World' if mimetype is 'text/plain'
return null
items: [{
kind: 'string'
type: 'text/plain'
getAsString: -> 'This is plain text'
getAsString: -> 'This is plain text\nAnother line Hello World'
}]
it "should sanitize the plain text string and call insertHTML", ->
spyOn(document, 'execCommand')
spyOn(@clipboardService, '_sanitizeInput').andCallThrough()
it "should convert the plain text to HTML and call insertHTML", ->
runs ->
@clipboardService.onPaste(@mockEvent)
waitsFor ->
document.execCommand.callCount > 0
runs ->
expect(@clipboardService._sanitizeInput).toHaveBeenCalledWith('This is plain text', 'text/html')
[command, a, html] = document.execCommand.mostRecentCall.args
expect(command).toEqual('insertHTML')
expect(html).toEqual('This is plain text')
expect(html).toEqual('This is plain text<br/>Another line &nbsp;Hello &nbsp;World')
describe "sanitization", ->
tests = [
{
in: ""
sanitizedAsHTML: ""
sanitizedAsPlain: ""
},
{
in: "Hello World"
sanitizedAsHTML: "Hello World"
sanitizedAsPlain: "Hello World"
},
{
in: " Hello World"
# Should collapse to 1 space when rendered
sanitizedAsHTML: " Hello World"
# Preserving 2 spaces
sanitizedAsPlain: " &nbsp;Hello &nbsp;World"
},
{
in: " Hello World"
sanitizedAsHTML: " Hello World"
# Preserving 3 spaces
sanitizedAsPlain: " &nbsp; Hello &nbsp; World"
},
{
in: " Hello World"
sanitizedAsHTML: " Hello World"
# Preserving 4 spaces
sanitizedAsPlain: " &nbsp; &nbsp;Hello &nbsp; &nbsp;World"
},
{
in: "Hello\nWorld"
sanitizedAsHTML: "Hello<br />World"
# Convert newline to br
sanitizedAsPlain: "Hello<br/>World"
},
{
in: "Hello\rWorld"
sanitizedAsHTML: "Hello<br />World"
# Convert carriage return to br
sanitizedAsPlain: "Hello<br/>World"
},
{
describe "HTML sanitization", ->
beforeEach ->
spyOn(InlineStyleTransformer, 'run').andCallThrough()
spyOn(SanitizeTransformer, 'run').andCallThrough()
it "should inline CSS styles and run the standard permissive HTML sanitizer", ->
input = "HTML HERE"
@clipboardService._sanitizeHTMLInput(input)
advanceClock()
expect(InlineStyleTransformer.run).toHaveBeenCalledWith(input)
advanceClock()
expect(SanitizeTransformer.run).toHaveBeenCalledWith(input, SanitizeTransformer.Preset.Permissive)
it "should replace two or more <br/>s in a row", ->
tests = [{
in: "Hello\n\n\nWorld"
# Never have more than 2 br's in a row
sanitizedAsHTML: "Hello<br/><br/>World"
# Convert multiple newlines to same number of brs
sanitizedAsPlain: "Hello<br/><br/><br/>World"
},
{
in: "<style>Yo</style> Foo Bar <div>Baz</div>"
# Strip bad tags
sanitizedAsHTML: " Foo Bar Baz"
# HTML encode tags for literal display
sanitizedAsPlain: "&lt;style&gt;Yo&lt;/style&gt; Foo Bar &lt;div&gt;Baz&lt;/div&gt;"
},
{
in: "<script>Bah</script> Yo < script>Boo! < / script >"
# Strip non white-list tags and encode malformed ones.
sanitizedAsHTML: " Yo &lt; script&gt;Boo! &lt; / script &gt;"
# HTML encode tags for literal display
sanitizedAsPlain: "&lt;script&gt;Bah&lt;/script&gt; Yo &lt; script&gt;Boo! &lt; / script &gt;"
},
{
in: """
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title></title>
<meta name="Generator" content="Cocoa HTML Writer">
<meta name="CocoaVersion" content="1265.21">
<style type="text/css">
li.li1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica}
ul.ul1 {list-style-type: disc}
</style>
</head>
<body>
<ul class="ul1">
<li class="li1"><b>Packet pickup: </b>I'll pick up my packet at some point on Saturday at Fort Mason. Let me know if you'd like me to get yours. I'll need a photo of your ID and your confirmation number. Also, shirt color preference, I believe. Gray or black? Can't remember...</li>
</ul>
</body>
</html>"""
# Strip non white-list tags and encode malformed ones.
sanitizedAsHTML: "<ul><br /><li><b>Packet pickup: </b>I'll pick up my packet at some point on Saturday at Fort Mason. Let me know if you'd like me to get yours. I'll need a photo of your ID and your confirmation number. Also, shirt color preference, I believe. Gray or black? Can't remember...</li><br /></ul>"
# HTML encode tags for literal display
sanitizedAsPlain: "&lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD HTML 4.01//EN&#34; &#34;http://www.w3.org/TR/html4/strict.dtd&#34;&gt;<br/>&lt;html&gt;<br/>&lt;head&gt;<br/>&lt;meta http-equiv=&#34;Content-Type&#34; content=&#34;text/html; charset=UTF-8&#34;&gt;<br/>&lt;meta http-equiv=&#34;Content-Style-Type&#34; content=&#34;text/css&#34;&gt;<br/>&lt;title&gt;&lt;/title&gt;<br/>&lt;meta name=&#34;Generator&#34; content=&#34;Cocoa HTML Writer&#34;&gt;<br/>&lt;meta name=&#34;CocoaVersion&#34; content=&#34;1265.21&#34;&gt;<br/>&lt;style type=&#34;text/css&#34;&gt;<br/>li.li1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica}<br/>ul.ul1 {list-style-type: disc}<br/>&lt;/style&gt;<br/>&lt;/head&gt;<br/>&lt;body&gt;<br/>&lt;ul class=&#34;ul1&#34;&gt;<br/>&lt;li class=&#34;li1&#34;&gt;&lt;b&gt;Packet pickup: &lt;/b&gt;I'll pick up my packet at some point on Saturday at Fort Mason. Let me know if you'd like me to get yours. I'll need a photo of your ID and your confirmation number. Also, shirt color preference, I believe. Gray or black? Can't remember...&lt;/li&gt;<br/>&lt;/ul&gt;<br/>&lt;/body&gt;<br/>&lt;/html&gt;"
}
]
it "sanitizes plain text properly", ->
out: "Hello<br/><br/>World"
},{
in: "Hello<br/><br/><br/><br/>World"
out: "Hello<br/><br/>World"
}]
for test in tests
expect(@clipboardService._sanitizeInput(test.in, "text/plain")).toBe test.sanitizedAsPlain
waitsForPromise =>
@clipboardService._sanitizeHTMLInput(test.in).then (out) ->
expect(out).toBe(test.out)
it "sanitizes html text properly", ->
it "should remove all leading and trailing <br/>s from the text", ->
tests = [{
in: "<br/><br/>Hello<br/>World"
out: "Hello<br/>World"
},{
in: "<br/><br/>Hello<br/><br/><br/><br/>"
out: "Hello"
}]
for test in tests
expect(@clipboardService._sanitizeInput(test.in, "text/html")).toBe test.sanitizedAsHTML
waitsForPromise =>
@clipboardService._sanitizeHTMLInput(test.in).then (out) ->
expect(out).toBe(test.out)
# Unfortunately, it doesn't seem we can do real IPC (to `juice` in the main process)
# so these tests are non-functional.
xdescribe "real-world examples", ->
it "should produce the correct output", ->
scenarios = []
fixtures = path.resolve('./spec/fixtures/paste')
for filename in fs.readdirSync(fixtures)
if filename[-8..-1] is '-in.html'
scenarios.push
in: fs.readFileSync(path.join(fixtures, filename)).toString()
out: fs.readFileSync(path.join(fixtures, "#{filename[0..-9]}-out.html")).toString()
scenarios.forEach (scenario) =>
@clipboardService._sanitizeHTMLInput(scenario.in).then (out) ->
expect(out).toBe(scenario.out)

279
spec/fixtures/paste/excel-paste-in.html vendored Normal file
View file

@ -0,0 +1,279 @@
"<html xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
<meta name=ProgId content=Excel.Sheet>
<meta name=Generator content="Microsoft Excel 14">
<link id=Main-File rel=Main-File
href="file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip.htm">
<link rel=File-List
href="file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_filelist.xml">
<!--[if !mso]>
<style>
v\:* {behavior:url(#default#VML);}
o\:* {behavior:url(#default#VML);}
x\:* {behavior:url(#default#VML);}
.shape {behavior:url(#default#VML);}
</style>
<![endif]-->
<style>
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
@page
{margin:1.0in .75in 1.0in .75in;
mso-header-margin:.5in;
mso-footer-margin:.5in;}
.font5
{color:black;
font-size:10.0pt;
font-weight:400;
font-style:normal;
text-decoration:none;
font-family:Geneva;
mso-generic-font-family:auto;
mso-font-charset:0;}
.font6
{color:black;
font-size:10.0pt;
font-weight:700;
font-style:normal;
text-decoration:none;
font-family:Geneva;
mso-generic-font-family:auto;
mso-font-charset:0;}
td
{padding:0px;
mso-ignore:padding;
color:black;
font-size:12.0pt;
font-weight:400;
font-style:normal;
text-decoration:none;
font-family:Calibri, sans-serif;
mso-font-charset:0;
mso-number-format:General;
text-align:general;
vertical-align:bottom;
border:none;
mso-background-source:auto;
mso-pattern:auto;
mso-protection:locked visible;
white-space:nowrap;
mso-rotate:0;}
.xl63
{vertical-align:middle;}
.xl64
{font-size:24.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;}
.xl65
{border-top:none;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl66
{border-top:none;
border-right:1.0pt solid windowtext;
border-bottom:none;
border-left:none;}
.xl67
{font-size:22.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;}
.xl68
{font-size:36.0pt;
font-weight:700;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;}
.xl69
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
vertical-align:middle;}
.xl70
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
vertical-align:middle;
border-top:none;
border-right:1.0pt solid windowtext;
border-bottom:none;
border-left:none;}
.xl71
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:left;
vertical-align:middle;
border-top:none;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl72
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
vertical-align:middle;
border-top:none;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl73
{font-size:26.0pt;
font-style:italic;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;
border-top:1.0pt solid windowtext;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl74
{font-size:26.0pt;
font-style:italic;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;
border-top:1.0pt solid windowtext;
border-right:1.0pt solid windowtext;
border-bottom:none;
border-left:none;}
-->
</style>
</head>
<body link=blue vlink=purple>
<table border=0 cellpadding=0 cellspacing=0 width=471 style='border-collapse:
collapse;width:471pt'>
<!--StartFragment-->
<col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>
<col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>
<col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>
<col width=67 style='mso-width-source:userset;mso-width-alt:2858;width:67pt'>
<col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>
<col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>
<col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>
<tr height=33 style='height:33.0pt'>
<td colspan=2 height=33 class=xl73 width=190 style='border-right:1.0pt solid black;
height:33.0pt;width:190pt'>+ Pros +</td>
<td class=xl64 width=12 style='width:12pt'></td>
<td class=xl67 width=67 style='width:67pt'>vs.</td>
<td width=12 style='width:12pt'></td>
<td colspan=2 class=xl73 width=190 style='border-right:1.0pt solid black;
width:190pt'>- Cons -</td>
</tr>
<tr height=15 style='height:15.0pt'>
<td height=15 class=xl65 style='height:15.0pt;font-size:12.0pt;color:white;
font-weight:700;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
1.0pt solid white;border-left:1.0pt solid windowtext;background:black;
mso-pattern:black none'>Item</td>
<td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:
black none'>Importance</td>
<td></td>
<td></td>
<td></td>
<td class=xl65 style='font-size:12.0pt;color:white;font-weight:700;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:1.0pt solid white;
border-left:1.0pt solid windowtext;background:black;mso-pattern:black none'>Item</td>
<td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:
black none'>Importance</td>
</tr>
<tr height=28 style='mso-height-source:userset;height:28.0pt'>
<td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;
font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Good</td>
<td class=xl69 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;background:#76933C;mso-pattern:#76933C none'>2</td>
<td class=xl63></td>
<td class=xl68></td>
<td class=xl63></td>
<td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:none;
border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Bad</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>2</td>
</tr>
<tr height=28 style='mso-height-source:userset;height:28.0pt'>
<td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;
font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
none;border-left:1.0pt solid windowtext;background:#9BBB59;mso-pattern:#9BBB59 none'>Cheap</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#9BBB59;mso-pattern:#9BBB59 none'>4</td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:none;
border-left:1.0pt solid windowtext;background:#C0504D;mso-pattern:#C0504D none'>Expensive</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#C0504D;mso-pattern:#C0504D none'>3</td>
</tr>
<tr height=28 style='mso-height-source:userset;height:28.0pt'>
<td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;
font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Fast</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#76933C;mso-pattern:#76933C none'>1</td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:none;
border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Slow</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>1</td>
</tr>
<!--EndFragment-->
</table>
</body>
</html>
"

279
spec/fixtures/paste/excel-paste-out.html vendored Normal file
View file

@ -0,0 +1,279 @@
"<html xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
<meta name=ProgId content=Excel.Sheet>
<meta name=Generator content="Microsoft Excel 14">
<link id=Main-File rel=Main-File
href="file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip.htm">
<link rel=File-List
href="file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_filelist.xml">
<!--[if !mso]>
<style>
v\:* {behavior:url(#default#VML);}
o\:* {behavior:url(#default#VML);}
x\:* {behavior:url(#default#VML);}
.shape {behavior:url(#default#VML);}
</style>
<![endif]-->
<style>
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
@page
{margin:1.0in .75in 1.0in .75in;
mso-header-margin:.5in;
mso-footer-margin:.5in;}
.font5
{color:black;
font-size:10.0pt;
font-weight:400;
font-style:normal;
text-decoration:none;
font-family:Geneva;
mso-generic-font-family:auto;
mso-font-charset:0;}
.font6
{color:black;
font-size:10.0pt;
font-weight:700;
font-style:normal;
text-decoration:none;
font-family:Geneva;
mso-generic-font-family:auto;
mso-font-charset:0;}
td
{padding:0px;
mso-ignore:padding;
color:black;
font-size:12.0pt;
font-weight:400;
font-style:normal;
text-decoration:none;
font-family:Calibri, sans-serif;
mso-font-charset:0;
mso-number-format:General;
text-align:general;
vertical-align:bottom;
border:none;
mso-background-source:auto;
mso-pattern:auto;
mso-protection:locked visible;
white-space:nowrap;
mso-rotate:0;}
.xl63
{vertical-align:middle;}
.xl64
{font-size:24.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;}
.xl65
{border-top:none;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl66
{border-top:none;
border-right:1.0pt solid windowtext;
border-bottom:none;
border-left:none;}
.xl67
{font-size:22.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;}
.xl68
{font-size:36.0pt;
font-weight:700;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;}
.xl69
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
vertical-align:middle;}
.xl70
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
vertical-align:middle;
border-top:none;
border-right:1.0pt solid windowtext;
border-bottom:none;
border-left:none;}
.xl71
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:left;
vertical-align:middle;
border-top:none;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl72
{font-size:14.0pt;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
vertical-align:middle;
border-top:none;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl73
{font-size:26.0pt;
font-style:italic;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;
border-top:1.0pt solid windowtext;
border-right:none;
border-bottom:none;
border-left:1.0pt solid windowtext;}
.xl74
{font-size:26.0pt;
font-style:italic;
font-family:Calibri;
mso-generic-font-family:auto;
mso-font-charset:0;
text-align:center;
vertical-align:middle;
border-top:1.0pt solid windowtext;
border-right:1.0pt solid windowtext;
border-bottom:none;
border-left:none;}
-->
</style>
</head>
<body link=blue vlink=purple>
<table border=0 cellpadding=0 cellspacing=0 width=471 style='border-collapse:
collapse;width:471pt'>
<!--StartFragment-->
<col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>
<col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>
<col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>
<col width=67 style='mso-width-source:userset;mso-width-alt:2858;width:67pt'>
<col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>
<col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>
<col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>
<tr height=33 style='height:33.0pt'>
<td colspan=2 height=33 class=xl73 width=190 style='border-right:1.0pt solid black;
height:33.0pt;width:190pt'>+ Pros +</td>
<td class=xl64 width=12 style='width:12pt'></td>
<td class=xl67 width=67 style='width:67pt'>vs.</td>
<td width=12 style='width:12pt'></td>
<td colspan=2 class=xl73 width=190 style='border-right:1.0pt solid black;
width:190pt'>- Cons -</td>
</tr>
<tr height=15 style='height:15.0pt'>
<td height=15 class=xl65 style='height:15.0pt;font-size:12.0pt;color:white;
font-weight:700;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
1.0pt solid white;border-left:1.0pt solid windowtext;background:black;
mso-pattern:black none'>Item</td>
<td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:
black none'>Importance</td>
<td></td>
<td></td>
<td></td>
<td class=xl65 style='font-size:12.0pt;color:white;font-weight:700;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:1.0pt solid white;
border-left:1.0pt solid windowtext;background:black;mso-pattern:black none'>Item</td>
<td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:
black none'>Importance</td>
</tr>
<tr height=28 style='mso-height-source:userset;height:28.0pt'>
<td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;
font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Good</td>
<td class=xl69 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;background:#76933C;mso-pattern:#76933C none'>2</td>
<td class=xl63></td>
<td class=xl68></td>
<td class=xl63></td>
<td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:none;
border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Bad</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>2</td>
</tr>
<tr height=28 style='mso-height-source:userset;height:28.0pt'>
<td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;
font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
none;border-left:1.0pt solid windowtext;background:#9BBB59;mso-pattern:#9BBB59 none'>Cheap</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#9BBB59;mso-pattern:#9BBB59 none'>4</td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:none;
border-left:1.0pt solid windowtext;background:#C0504D;mso-pattern:#C0504D none'>Expensive</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#C0504D;mso-pattern:#C0504D none'>3</td>
</tr>
<tr height=28 style='mso-height-source:userset;height:28.0pt'>
<td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;
font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:
none;font-family:Calibri;border-top:none;border-right:none;border-bottom:
none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Fast</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#76933C;mso-pattern:#76933C none'>1</td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl63></td>
<td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;
text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:none;border-bottom:none;
border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Slow</td>
<td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:
400;text-decoration:none;text-underline-style:none;text-line-through:none;
font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;
border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>1</td>
</tr>
<!--EndFragment-->
</table>
</body>
</html>
"

1306
spec/fixtures/paste/word-paste-in.html vendored Normal file

File diff suppressed because it is too large Load diff

1306
spec/fixtures/paste/word-paste-out.html vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,22 @@
_ = require('underscore')
fs = require('fs')
path = require 'path'
QuotedHTMLParser = require('../src/services/quoted-html-parser')
QuotedHTMLTransformer = require('../src/services/quoted-html-transformer')
describe "QuotedHTMLParser", ->
describe "QuotedHTMLTransformer", ->
readFile = (fname) ->
emailPath = path.resolve(__dirname, 'fixtures', 'emails', fname)
return fs.readFileSync(emailPath, 'utf8')
hideQuotedHTML = (fname) ->
return QuotedHTMLParser.hideQuotedHTML(readFile(fname))
return QuotedHTMLTransformer.hideQuotedHTML(readFile(fname))
removeQuotedHTML = (fname, opts={}) ->
return QuotedHTMLParser.removeQuotedHTML(readFile(fname), opts)
return QuotedHTMLTransformer.removeQuotedHTML(readFile(fname), opts)
numQuotes = (html) ->
re = new RegExp(QuotedHTMLParser.annotationClass, 'g')
re = new RegExp(QuotedHTMLTransformer.annotationClass, 'g')
html.match(re)?.length ? 0
[1..16].forEach (n) ->
@ -260,23 +260,23 @@ describe "QuotedHTMLParser", ->
it 'works with these manual test cases', ->
for {before, after} in tests
opts = keepIfWholeBodyIsQuote: true
test = clean(QuotedHTMLParser.removeQuotedHTML(before, opts))
test = clean(QuotedHTMLTransformer.removeQuotedHTML(before, opts))
expect(test).toEqual clean(after)
it 'removes all trailing <br> tags except one', ->
input0 = "hello world<br><br><blockquote>foolololol</blockquote>"
expect0 = "<head></head><body>hello world<br></body>"
expect(QuotedHTMLParser.removeQuotedHTML(input0)).toEqual expect0
expect(QuotedHTMLTransformer.removeQuotedHTML(input0)).toEqual expect0
it 'preserves <br> tags in the middle and only chops off tail', ->
input0 = "hello<br><br>world<br><br><blockquote>foolololol</blockquote>"
expect0 = "<head></head><body>hello<br><br>world<br></body>"
expect(QuotedHTMLParser.removeQuotedHTML(input0)).toEqual expect0
expect(QuotedHTMLTransformer.removeQuotedHTML(input0)).toEqual expect0
# We have a little utility method that you can manually uncomment to
# generate what the current iteration of the QuotedHTMLParser things the
# generate what the current iteration of the QuotedHTMLTransformer things the
# `removeQuotedHTML` should look like. These can be manually inspected in
# a browser before getting their filename changed to
# `email_#{n}_stripped.html". The actually tests will run the current
@ -284,11 +284,10 @@ describe "QuotedHTMLParser", ->
# anything has changed in the parser.
#
# It's inside of the specs here instaed of its own script because the
# `QuotedHTMLParser` needs Electron booted up in order to work because
# `QuotedHTMLTransformer` needs Electron booted up in order to work because
# of the DOMParser.
xit "Run this simple funciton to generate output files", ->
[16].forEach (n) ->
newHTML = QuotedHTMLParser.removeQuotedHTML(readFile("email_#{n}.html"))
newHTML = QuotedHTMLTransformer.removeQuotedHTML(readFile("email_#{n}.html"))
outPath = path.resolve(__dirname, 'fixtures', 'emails', "email_#{n}_raw_stripped.html")
fs.writeFileSync(outPath, newHTML)

View file

@ -4,7 +4,7 @@
fs = require('fs')
path = require 'path'
_ = require('underscore')
QuotedPlainTextParser = require('../src/services/quoted-plain-text-parser')
QuotedPlainTextParser = require('../src/services/quoted-plain-text-transformer')
getParsedEmail = (name, format="plain") ->
data = getRawEmail(name, format)

View file

@ -0,0 +1,67 @@
InlineStyleTransformer = require '../../src/services/inline-style-transformer'
{ipcRenderer} = require 'electron'
describe "InlineStyleTransformer", ->
describe "run", ->
beforeEach ->
spyOn(ipcRenderer, 'send')
spyOn(InlineStyleTransformer, '_injectUserAgentStyles').andCallFake (input) =>
return input
InlineStyleTransformer._inlineStylePromises = {}
it "should return a Promise", ->
expect(InlineStyleTransformer.run("asd") instanceof Promise).toBe(true)
it "should resolve immediately if the html is empty", ->
result = InlineStyleTransformer.run("")
expect(result.isResolved()).toBe(true)
it "should resolve immediately if there is no <style> tag in the source", ->
result = InlineStyleTransformer.run("""
This is some tricky HTML but there's no style tag here!
<I wonder if it'll get into trouble < style >. <Ohmgerd.>
""")
expect(result.isResolved()).toBe(true)
it "should properly remove comment tags used to prevent style tags from being displayed when they're not understood", ->
result = InlineStyleTransformer.run("""
<style>
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
-->
</style>
<style><!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
--></style>
""")
expect(ipcRenderer.send.mostRecentCall.args[1].html).toEqual("""
<style>table
{mso-displayed-decimal-separator:".";
mso-displayed-thousand-separator:",";}
</style>
<style>table
{mso-displayed-decimal-separator:".";
mso-displayed-thousand-separator:",";}
</style>
""")
it "should add user agent styles", ->
InlineStyleTransformer.run("""<style>
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
-->
</style>Other content goes here""")
expect(InlineStyleTransformer._injectUserAgentStyles).toHaveBeenCalled()
it "should fire inline-style-parse to the main process", ->
InlineStyleTransformer.run("""<style>
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
-->
</style>Other content goes here""")
expect(ipcRenderer.send).toHaveBeenCalled()
expect(ipcRenderer.send.mostRecentCall.args[0]).toEqual('inline-style-parse')

View file

@ -13,6 +13,9 @@ SoundRegistry = require '../../src/sound-registry'
Actions = require '../../src/flux/actions'
Utils = require '../../src/flux/models/utils'
InlineStyleTransformer = require '../../src/services/inline-style-transformer'
SanitizeTransformer = require '../../src/services/sanitize-transformer'
{ipcRenderer} = require 'electron'
_ = require 'underscore'
@ -40,9 +43,8 @@ describe "DraftStore", ->
describe "creating drafts", ->
beforeEach ->
spyOn(DraftStore, "_sanitizeBody").andCallThrough()
spyOn(DraftStore, "_onInlineStylesResult").andCallThrough()
spyOn(DraftStore, "_convertToInlineStyles").andCallThrough()
spyOn(DraftStore, "_prepareBodyForQuoting").andCallFake (body) ->
Promise.resolve(body)
spyOn(ipcRenderer, "send").andCallFake (message, body) ->
if message is "inline-style-parse"
# There needs to be a defer block in here so the promise
@ -181,11 +183,7 @@ describe "DraftStore", ->
expect(@model.replyToMessageId).toEqual(fakeMessage1.id)
it "should sanitize the HTML", ->
expect(DraftStore._sanitizeBody).toHaveBeenCalled()
it "should not call the style inliner when there are no style tags", ->
expect(DraftStore._convertToInlineStyles).not.toHaveBeenCalled()
expect(DraftStore._onInlineStylesResult).not.toHaveBeenCalled()
expect(DraftStore._prepareBodyForQuoting).toHaveBeenCalled()
describe "onComposeReply", ->
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
@ -245,11 +243,7 @@ describe "DraftStore", ->
expect(@model.replyToMessageId).toEqual(fakeMessage1.id)
it "should sanitize the HTML", ->
expect(DraftStore._sanitizeBody).toHaveBeenCalled()
it "should not call the style inliner when there are no style tags", ->
expect(DraftStore._convertToInlineStyles).not.toHaveBeenCalled()
expect(DraftStore._onInlineStylesResult).not.toHaveBeenCalled()
expect(DraftStore._prepareBodyForQuoting).toHaveBeenCalled()
describe "onComposeReplyAll", ->
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
@ -329,45 +323,7 @@ describe "DraftStore", ->
expect(@model.replyToMessageId).toEqual(undefined)
it "should sanitize the HTML", ->
expect(DraftStore._sanitizeBody).toHaveBeenCalled()
it "should not call the style inliner when there are no style tags", ->
expect(DraftStore._convertToInlineStyles).not.toHaveBeenCalled()
expect(DraftStore._onInlineStylesResult).not.toHaveBeenCalled()
describe "inlining <style> tags", ->
it "inlines styles when replying", ->
runs ->
DraftStore._onComposeReply({threadId: fakeThread.id, messageId: messageWithStyleTags.id})
advanceClock(100)
waitsFor ->
DatabaseStore.persistModel.callCount > 0
runs ->
model = DatabaseStore.persistModel.mostRecentCall.args[0]
expect(DraftStore._convertToInlineStyles).toHaveBeenCalled()
expect(DraftStore._onInlineStylesResult).toHaveBeenCalled()
it "inlines styles when replying all", ->
runs ->
DraftStore._onComposeReplyAll({threadId: fakeThread.id, messageId: messageWithStyleTags.id})
advanceClock(100)
waitsFor ->
DatabaseStore.persistModel.callCount > 0
runs ->
model = DatabaseStore.persistModel.mostRecentCall.args[0]
expect(DraftStore._convertToInlineStyles).toHaveBeenCalled()
expect(DraftStore._onInlineStylesResult).toHaveBeenCalled()
it "inlines styles when forwarding", ->
runs ->
DraftStore._onComposeForward({threadId: fakeThread.id, messageId: messageWithStyleTags.id})
advanceClock(100)
waitsFor ->
DatabaseStore.persistModel.callCount > 0
runs ->
model = DatabaseStore.persistModel.mostRecentCall.args[0]
expect(DraftStore._convertToInlineStyles).toHaveBeenCalled()
expect(DraftStore._onInlineStylesResult).toHaveBeenCalled()
expect(DraftStore._prepareBodyForQuoting).toHaveBeenCalled()
describe "popout drafts", ->
beforeEach ->
@ -583,6 +539,17 @@ describe "DraftStore", ->
expect(message).toEqual(fakeMessage1)
{}
describe "sanitizing draft bodies", ->
it "should transform inline styles and sanitize unsafe html", ->
spyOn(InlineStyleTransformer, 'run').andCallFake (input) => Promise.resolve(input)
spyOn(SanitizeTransformer, 'run').andCallFake (input) => Promise.resolve(input)
input = "test 123"
DraftStore._prepareBodyForQuoting(input)
expect(InlineStyleTransformer.run).toHaveBeenCalledWith(input)
advanceClock()
expect(SanitizeTransformer.run).toHaveBeenCalledWith(input, SanitizeTransformer.Preset.UnsafeOnly)
describe "onDestroyDraft", ->
beforeEach ->
@draftSessionTeardown = jasmine.createSpy('draft teardown')

View file

@ -328,18 +328,18 @@ class Application
ipcMain.on 'from-react-remote-window-selection', (event, json) =>
@windowManager.sendToMainWindow('from-react-remote-window-selection', json)
ipcMain.on 'inline-style-parse', (event, {body, clientId}) =>
ipcMain.on 'inline-style-parse', (event, {html, key}) =>
juice = require 'juice'
try
body = juice(body)
html = juice(html)
catch
# If the juicer fails (because of malformed CSS or some other
# reason), then just return the body. We will still push it
# through the HTML sanitizer which will strip the style tags. Oh
# well.
body = body
html = html
# win = BrowserWindow.fromWebContents(event.sender)
event.sender.send('inline-styles-result', {body, clientId})
event.sender.send('inline-styles-result', {html, key})
app.on 'activate', (event, hasVisibleWindows) =>
if not hasVisibleWindows

View file

@ -1,16 +1,17 @@
{Utils} = require 'nylas-exports'
sanitizeHtml = require 'sanitize-html'
{InlineStyleTransformer,
SanitizeTransformer,
Utils} = require 'nylas-exports'
class ClipboardService
constructor: ({@onFilePaste}={}) ->
onPaste: (evt) =>
return if evt.clipboardData.items.length is 0
evt.preventDefault()
onPaste: (event) =>
return if event.clipboardData.items.length is 0
event.preventDefault()
# If the pasteboard has a file on it, stream it to a teporary
# file and fire our `onFilePaste` event.
item = evt.clipboardData.items[0]
item = event.clipboardData.items[0]
if item.kind is 'file'
blob = item.getAsFile()
@ -30,62 +31,46 @@ class ClipboardService
reader.readAsArrayBuffer(blob)
else
# Look for text/html in any of the clipboard items and fall
# back to text/plain.
inputText = evt.clipboardData.getData("text/html") ? ""
type = "text/html"
if inputText.length is 0
inputText = evt.clipboardData.getData("text/plain") ? ""
type = "text/plain"
{input, mimetype} = @_getBestRepresentation(event.clipboardData)
if inputText.length > 0
cleanHtml = @_sanitizeInput(inputText, type)
document.execCommand("insertHTML", false, cleanHtml)
if mimetype is 'text/plain'
input = Utils.encodeHTMLEntities(input)
input = input.replace(/[\r\n]|&#1[03];/g, "<br/>").replace(/\s\s/g, " &nbsp;")
document.execCommand("insertHTML", false, input)
else if mimetype is 'text/html'
@_sanitizeHTMLInput(input).then (cleanHtml) ->
document.execCommand("insertHTML", false, cleanHtml)
else
# Do nothing. No appropriate format is available
return
_getBestRepresentation: (clipboardData) =>
for mimetype in ["text/html", "text/plain"]
data = clipboardData.getData(mimetype) ? ""
if data.length > 0
return {input: data, mimetype: mimetype}
return {input: null, mimetype: null}
# This is used primarily when pasting text in
_sanitizeInput: (inputText="", type="text/html") =>
if type is "text/plain"
inputText = Utils.encodeHTMLEntities(inputText)
inputText = inputText.replace(/[\r\n]|&#1[03];/g, "<br/>").
replace(/\s\s/g, " &nbsp;")
else
inputText = sanitizeHtml inputText.replace(/[\n\r]/g, "<br/>"),
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike']
allowedAttributes:
a: ['href', 'name']
img: ['src', 'alt']
transformTags:
h1: "p"
h2: "p"
h3: "p"
h4: "p"
h5: "p"
h6: "p"
div: "p"
pre: "p"
blockquote: "p"
table: "p"
_sanitizeHTMLInput: (input) =>
InlineStyleTransformer.run(input).then (input) =>
SanitizeTransformer.run(input, SanitizeTransformer.Preset.Permissive).then (input) =>
# We never want more then 2 line breaks in a row.
# https://regex101.com/r/gF6bF4/4
input = input.replace(/(<br\s*\/?>\s*){3,}/g, "<br/><br/>")
# We sanitized everything and convert all whitespace-inducing
# elements into <p> tags. We want to de-wrap <p> tags and replace
# with two line breaks instead.
inputText = inputText.replace(/<p[\s\S]*?>/gim, "").
replace(/<\/p>/gi, "<br/>")
# We never want to keep leading and trailing <brs>, since the user
# would have started a new paragraph themselves if they wanted space
# before what they paste.
# BAD: "<p>begins at<br>12AM</p>" => "<br><br>begins at<br>12AM<br><br>"
# Better: "<p>begins at<br>12AM</p>" => "begins at<br>12"
input = input.replace(/^(<br ?\/>)+/, '')
input = input.replace(/(<br ?\/>)+$/, '')
# We never want more then 2 line breaks in a row.
# https://regex101.com/r/gF6bF4/4
inputText = inputText.replace(/(<br\s*\/?>\s*){3,}/g, "<br/><br/>")
# We never want to keep leading and trailing <brs>, since the user
# would have started a new paragraph themselves if they wanted space
# before what they paste.
# BAD: "<p>begins at<br>12AM</p>" => "<br><br>begins at<br>12AM<br><br>"
# Better: "<p>begins at<br>12AM</p>" => "begins at<br>12"
inputText = inputText.replace(/^(<br ?\/>)+/, '')
inputText = inputText.replace(/(<br ?\/>)+$/, '')
return inputText
Promise.resolve(input)
module.exports = ClipboardService

View file

@ -648,6 +648,9 @@ class Contenteditable extends React.Component
menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))
menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))
menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))
menu.append(new MenuItem({ label: 'Paste and Match Style', click: =>
NylasEnv.getCurrentWindow().webContents.pasteAndMatchStyle()
}))
menu.popup(remote.getCurrentWindow())
_onMouseDown: (event) =>

View file

@ -506,20 +506,4 @@ DOMUtils =
return 0
# This allows pretty much everything except:
# script, embed, head, html, iframe, link, style, base
# Comes form React's support HTML elements: https://facebook.github.io/react/docs/tags-and-attributes.html
permissiveTags: -> ["a", "abbr", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "i", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"]
# Comes form React's support HTML elements: https://facebook.github.io/react/docs/tags-and-attributes.html
# Removed: class
allAttributes: [ 'abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'classid', 'classname', 'colspan', 'cols', 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'data', 'datetime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'enctype', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'frame', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'htmlfor', 'httpequiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginheight', 'marginwidth', 'max', 'maxlength', 'media', 'mediagroup', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'rel', 'required', 'role', 'rowspan', 'rows', 'rules', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'sortable', 'sorted', 'span', 'spellcheck', 'src', 'srcdoc', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wmode' ]
# Allows any attribute on any tag.
permissiveAttributes: ->
allAttrMap = {}
for tag in DOMUtils.permissiveTags()
allAttrMap[tag] = DOMUtils.allAttributes
return allAttrMap
module.exports = DOMUtils

View file

@ -1,7 +1,6 @@
_ = require 'underscore'
crypto = require 'crypto'
moment = require 'moment'
sanitizeHtml = require 'sanitize-html'
{ipcRenderer} = require 'electron'
@ -13,6 +12,9 @@ ContactStore = require './contact-store'
SendDraftTask = require '../tasks/send-draft'
DestroyDraftTask = require '../tasks/destroy-draft'
InlineStyleTransformer = require '../../services/inline-style-transformer'
SanitizeTransformer = require '../../services/sanitize-transformer'
Thread = require '../models/thread'
Contact = require '../models/contact'
Message = require '../models/message'
@ -27,7 +29,6 @@ SoundRegistry = require '../../sound-registry'
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
DOMUtils = require '../../dom-utils'
RegExpUtils = require '../../regexp-utils'
ExtensionRegistry = require '../../extension-registry'
{deprecate} = require '../../deprecate-utils'
@ -76,9 +77,6 @@ class DraftStore
@_draftSessions = {}
@_inlineStylePromises = {}
@_inlineStyleResolvers = {}
# We would ideally like to be able to calculate the sending state
# declaratively from the existence of the SendDraftTask on the
# TaskQueue.
@ -97,8 +95,6 @@ class DraftStore
ipcRenderer.on 'mailto', @_onHandleMailtoLink
ipcRenderer.on 'inline-styles-result', @_onInlineStylesResult
######### PUBLIC #######################################################
# Public: Fetch a {DraftStoreProxy} for displaying and/or editing the
@ -323,7 +319,7 @@ class DraftStore
_prepareAttributesBody: (attributes) ->
if attributes.replyToMessage
replyToMessage = attributes.replyToMessage
@_prepareBodyForQuoting(replyToMessage.body, attributes.clientId).then (body) ->
@_prepareBodyForQuoting(replyToMessage.body).then (body) ->
return """
<br><br><blockquote class="gmail_quote"
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
@ -342,7 +338,7 @@ class DraftStore
fields.push("To: #{contactsAsHtml(forwardMessage.to)}") if forwardMessage.to.length > 0
fields.push("CC: #{contactsAsHtml(forwardMessage.cc)}") if forwardMessage.cc.length > 0
fields.push("BCC: #{contactsAsHtml(forwardMessage.bcc)}") if forwardMessage.bcc.length > 0
@_prepareBodyForQuoting(forwardMessage.body, attributes.clientId).then (body) ->
@_prepareBodyForQuoting(forwardMessage.body).then (body) ->
return """
<br><br><blockquote class="gmail_quote"
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
@ -355,50 +351,17 @@ class DraftStore
else return Promise.resolve("")
# Eventually we'll want a nicer solution for inline attachments
_prepareBodyForQuoting: (body="", clientId) =>
_prepareBodyForQuoting: (body="") =>
## Fix inline images
cidRE = MessageUtils.cidRegexString
# Be sure to match over multiple lines with [\s\S]*
# Regex explanation here: https://regex101.com/r/vO6eN2/1
re = new RegExp("<img.*#{cidRE}[\\s\\S]*?>", "igm")
body.replace(re, "")
## Remove style tags and inline styles
# This prevents styles from leaking emails.
# https://github.com/Automattic/juice
if (RegExpUtils.looseStyleTag()).test(body)
@_convertToInlineStyles(body, clientId).then (body) =>
return @_sanitizeBody(body)
else
return Promise.resolve(@_sanitizeBody(body))
_convertToInlineStyles: (body, clientId) ->
body = @_injectUserAgentStyles(body)
@_inlineStylePromises[clientId] ?= new Promise (resolve, reject) =>
@_inlineStyleResolvers[clientId] = resolve
ipcRenderer.send('inline-style-parse', {body, clientId})
return @_inlineStylePromises[clientId]
# This will prepend the user agent stylesheet so we can apply it to the
# styles properly.
_injectUserAgentStyles: (body) ->
# No DOM parsing! Just find the first <style> tag and prepend there.
i = body.search(RegExpUtils.looseStyleTag())
return body if i is -1
userAgentDefault = require '../../chrome-user-agent-stylesheet-string'
return "#{body[0...i]}<style>#{userAgentDefault}</style>#{body[i..-1]}"
_onInlineStylesResult: (event, {body, clientId}) =>
delete @_inlineStylePromises[clientId]
@_inlineStyleResolvers[clientId](body)
delete @_inlineStyleResolvers[clientId]
return
_sanitizeBody: (body) ->
return sanitizeHtml body,
allowedTags: DOMUtils.permissiveTags()
allowedAttributes: DOMUtils.permissiveAttributes()
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'data' ]
InlineStyleTransformer.run(body).then (body) =>
SanitizeTransformer.run(body, SanitizeTransformer.Preset.UnsafeOnly)
_onPopoutBlankDraft: =>
account = AccountStore.current()

View file

@ -83,4 +83,5 @@ class MessageBodyProcessor
@_recentlyProcessedA.unshift(item)
@_recentlyProcessedD[key] = item
module.exports = new MessageBodyProcessor()

View file

@ -151,10 +151,15 @@ class NylasExports
# Services
@load "UndoManager", 'undo-manager'
@load "SoundRegistry", 'sound-registry'
@load "QuotedHTMLParser", 'services/quoted-html-parser'
@load "QuotedPlainTextParser", 'services/quoted-plain-text-parser'
@load "NativeNotifications", 'native-notifications'
@load "QuotedHTMLTransformer", 'services/quoted-html-transformer'
@load "QuotedPlainTextTransformer", 'services/quoted-plain-text-transformer'
@load "SanitizeTransformer", 'services/sanitize-transformer'
@load "InlineStyleTransformer", 'services/inline-style-transformer'
@requireDeprecated "QuotedHTMLParser", 'services/quoted-html-transformer',
instead: 'QuotedHTMLTransformer'
# Errors
@get "APIError", -> require('../flux/errors').APIError
@get "OfflineError", -> require('../flux/errors').OfflineError

View file

@ -0,0 +1,46 @@
{ipcRenderer} = require "electron"
RegExpUtils = require '../regexp-utils'
crypto = require 'crypto'
_ = require 'underscore'
class InlineStyleTransformer
constructor: ->
@_inlineStylePromises = {}
@_inlineStyleResolvers = {}
ipcRenderer.on 'inline-styles-result', @_onInlineStylesResult
run: (html) =>
return Promise.resolve(html) unless html and _.isString(html) and html.length > 0
return Promise.resolve(html) unless RegExpUtils.looseStyleTag().test(html)
key = crypto.createHash('md5').update(html).digest('hex')
# http://stackoverflow.com/questions/8695031/why-is-there-often-a-inside-the-style-tag
# https://regex101.com/r/bZ5tX4/1
html = html.replace /<style[^>]*>[\n\r]*<!--([^<\/]*)-->[\n\r]*<\/style/g, (full, content) ->
"<style>#{content}</style"
html = @_injectUserAgentStyles(html)
@_inlineStylePromises[key] ?= new Promise (resolve, reject) =>
@_inlineStyleResolvers[key] = resolve
ipcRenderer.send('inline-style-parse', {html, key})
return @_inlineStylePromises[key]
# This will prepend the user agent stylesheet so we can apply it to the
# styles properly.
_injectUserAgentStyles: (body) ->
# No DOM parsing! Just find the first <style> tag and prepend there.
i = body.search(RegExpUtils.looseStyleTag())
return body if i is -1
userAgentDefault = require '../chrome-user-agent-stylesheet-string'
return "#{body[0...i]}<style>#{userAgentDefault}</style>#{body[i..-1]}"
_onInlineStylesResult: (event, {html, key}) =>
delete @_inlineStylePromises[key]
@_inlineStyleResolvers[key](html)
delete @_inlineStyleResolvers[key]
return
module.exports = new InlineStyleTransformer()

View file

@ -2,7 +2,7 @@ _ = require 'underscore'
crypto = require 'crypto'
DOMUtils = require '../dom-utils'
class QuotedHTMLParser
class QuotedHTMLTransformer
annotationClass: "nylas-quoted-text-segment"
@ -198,4 +198,4 @@ class QuotedHTMLParser
return Array::slice.call(doc.querySelectorAll('blockquote'))
module.exports = new QuotedHTMLParser
module.exports = new QuotedHTMLTransformer

View file

@ -6,7 +6,7 @@ _str = require 'underscore.string'
# For plain text emails we look for lines that look like they're quoted
# text based on common conventions:
#
# For HTML emails use QuotedHTMLParser
# For HTML emails use QuotedHTMLTransformer
#
# This is modied from https://github.com/mko/emailreplyparser, which is a
# JS port of GitHub's Ruby https://github.com/github/email_reply_parser

View file

@ -0,0 +1,42 @@
sanitizeHtml = require 'sanitize-html'
Preset =
Strict:
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike', 'table']
allowedAttributes:
a: ['href', 'name']
img: ['src', 'alt']
transformTags:
h1: "p"
h2: "p"
h3: "p"
h4: "p"
h5: "p"
h6: "p"
div: "p"
pre: "p"
blockquote: "p"
Permissive:
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike', 'table', 'tr', 'td', 'th', 'col', 'colgroup']
allowedAttributes: [ 'abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'classid', 'classname', 'colspan', 'cols', 'content', 'contextmenu', 'controls', 'coords', 'data', 'datetime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'enctype', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'frame', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'htmlfor', 'httpequiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginheight', 'marginwidth', 'max', 'maxlength', 'media', 'mediagroup', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'rel', 'required', 'role', 'rowspan', 'rows', 'rules', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'sortable', 'sorted', 'span', 'spellcheck', 'src', 'srcdoc', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wmode' ]
UnsafeOnly:
allowedTags: ["a", "abbr", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "i", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"]
allowedAttributes: [ 'abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'classid', 'classname', 'colspan', 'cols', 'content', 'contextmenu', 'controls', 'coords', 'data', 'datetime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'enctype', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'frame', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'htmlfor', 'httpequiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginheight', 'marginwidth', 'max', 'maxlength', 'media', 'mediagroup', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'rel', 'required', 'role', 'rowspan', 'rows', 'rules', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'sortable', 'sorted', 'span', 'spellcheck', 'src', 'srcdoc', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wmode' ]
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'data' ]
class SanitizeTransformer
Preset: Preset
run: (body, settings = Preset.Strict) ->
if settings.allowedAttributes instanceof Array
attrMap = {}
for tag in settings.allowedTags
attrMap[tag] = settings.allowedAttributes
settings.allowedAttributes = attrMap
return Promise.resolve(sanitizeHtml(body, settings))
module.exports = new SanitizeTransformer()

View file

@ -125,6 +125,7 @@ class WindowEventHandler
bindCommandToAction('core:copy', 'copy')
bindCommandToAction('core:cut', 'cut')
bindCommandToAction('core:paste', 'paste')
bindCommandToAction('core:paste-and-match-style', 'pasteAndMatchStyle')
bindCommandToAction('core:undo', 'undo')
bindCommandToAction('core:redo', 'redo')
bindCommandToAction('core:select-all', 'selectAll')