mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-17 22:42:31 +08:00
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:
parent
d8c79a4d50
commit
a1bb98ebf4
34 changed files with 3518 additions and 328 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
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">"<br/>"</span>+<span class="hljs-property">@_createBlock</span>(events,eventData)+<span class="hljs-string">"<br/>"</span>;
|
||||
|
||||
newDraftHtml = QuotedHTMLParser.appendQuotedHTML(text, draftHtml)</pre></div></div>
|
||||
|
||||
newDraftHtml = QuotedHTMLTransformer.appendQuotedHTML(text, draftHtml)</pre></div></div>
|
||||
</li>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">•••</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: =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">•••</span>{text} previous
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ MessageControls = require './message-controls'
|
|||
AccountStore,
|
||||
MessageStore,
|
||||
MessageBodyProcessor,
|
||||
QuotedHTMLParser,
|
||||
QuotedHTMLTransformer,
|
||||
ComponentRegistry,
|
||||
FileDownloadStore} = require 'nylas-exports'
|
||||
{RetinaImg,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ ReactTestUtils = React.addons.TestUtils
|
|||
File,
|
||||
Thread,
|
||||
Utils,
|
||||
QuotedHTMLParser,
|
||||
QuotedHTMLTransformer,
|
||||
FileDownloadStore,
|
||||
MessageBodyProcessor} = require "nylas-exports"
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 Hello 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: " Hello World"
|
||||
},
|
||||
{
|
||||
in: " Hello World"
|
||||
sanitizedAsHTML: " Hello World"
|
||||
# Preserving 3 spaces
|
||||
sanitizedAsPlain: " Hello World"
|
||||
},
|
||||
{
|
||||
in: " Hello World"
|
||||
sanitizedAsHTML: " Hello World"
|
||||
# Preserving 4 spaces
|
||||
sanitizedAsPlain: " Hello 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: "<style>Yo</style> Foo Bar <div>Baz</div>"
|
||||
},
|
||||
{
|
||||
in: "<script>Bah</script> Yo < script>Boo! < / script >"
|
||||
# Strip non white-list tags and encode malformed ones.
|
||||
sanitizedAsHTML: " Yo < script>Boo! < / script >"
|
||||
# HTML encode tags for literal display
|
||||
sanitizedAsPlain: "<script>Bah</script> Yo < script>Boo! < / script >"
|
||||
},
|
||||
{
|
||||
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: "<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><br/><html><br/><head><br/><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><br/><meta http-equiv="Content-Style-Type" content="text/css"><br/><title></title><br/><meta name="Generator" content="Cocoa HTML Writer"><br/><meta name="CocoaVersion" content="1265.21"><br/><style type="text/css"><br/>li.li1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica}<br/>ul.ul1 {list-style-type: disc}<br/></style><br/></head><br/><body><br/><ul class="ul1"><br/><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><br/></ul><br/></body><br/></html>"
|
||||
}
|
||||
]
|
||||
|
||||
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
279
spec/fixtures/paste/excel-paste-in.html
vendored
Normal 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
279
spec/fixtures/paste/excel-paste-out.html
vendored
Normal 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
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
1306
spec/fixtures/paste/word-paste-out.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
67
spec/services/inline-style-transformer-spec.coffee
Normal file
67
spec/services/inline-style-transformer-spec.coffee
Normal 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')
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]|[03];/g, "<br/>").replace(/\s\s/g, " ")
|
||||
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]|[03];/g, "<br/>").
|
||||
replace(/\s\s/g, " ")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -83,4 +83,5 @@ class MessageBodyProcessor
|
|||
@_recentlyProcessedA.unshift(item)
|
||||
@_recentlyProcessedD[key] = item
|
||||
|
||||
|
||||
module.exports = new MessageBodyProcessor()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
46
src/services/inline-style-transformer.coffee
Normal file
46
src/services/inline-style-transformer.coffee
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
42
src/services/sanitize-transformer.coffee
Normal file
42
src/services/sanitize-transformer.coffee
Normal 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()
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue