mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-30 20:07:48 +08:00
feat(composer): sanitizes and inlines CSS styles for replies and fwd
This commit is contained in:
parent
be697e42c7
commit
31714407f9
7 changed files with 1127 additions and 53 deletions
|
@ -17,6 +17,7 @@
|
||||||
"6to5-core": "^3.5",
|
"6to5-core": "^3.5",
|
||||||
"async": "^0.9",
|
"async": "^0.9",
|
||||||
"atom-keymap": "^5.1",
|
"atom-keymap": "^5.1",
|
||||||
|
"aws-sdk": "2.1.28",
|
||||||
"bluebird": "^2.9",
|
"bluebird": "^2.9",
|
||||||
"clear-cut": "0.4.0",
|
"clear-cut": "0.4.0",
|
||||||
"coffee-react": "^2.0.0",
|
"coffee-react": "^2.0.0",
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
"jasmine-json": "~0.0",
|
"jasmine-json": "~0.0",
|
||||||
"jasmine-tagged": "^1.1.2",
|
"jasmine-tagged": "^1.1.2",
|
||||||
"jquery": "^2.1.1",
|
"jquery": "^2.1.1",
|
||||||
|
"juice": "^1.4",
|
||||||
"less-cache": "0.21",
|
"less-cache": "0.21",
|
||||||
"marked": "^0.3",
|
"marked": "^0.3",
|
||||||
"mkdirp": "^0.5",
|
"mkdirp": "^0.5",
|
||||||
|
@ -57,7 +59,7 @@
|
||||||
"request": "^2.53",
|
"request": "^2.53",
|
||||||
"request-progress": "^0.3",
|
"request-progress": "^0.3",
|
||||||
"runas": "^2.0",
|
"runas": "^2.0",
|
||||||
"sanitize-html": "^1.5",
|
"sanitize-html": "^1.9",
|
||||||
"scandal": "2.0.0",
|
"scandal": "2.0.0",
|
||||||
"scoped-property-store": "^0.16.2",
|
"scoped-property-store": "^0.16.2",
|
||||||
"scrollbar-style": "^2.0",
|
"scrollbar-style": "^2.0",
|
||||||
|
@ -66,15 +68,14 @@
|
||||||
"serializable": "^1",
|
"serializable": "^1",
|
||||||
"service-hub": "^0.2.0",
|
"service-hub": "^0.2.0",
|
||||||
"space-pen": "3.8.2",
|
"space-pen": "3.8.2",
|
||||||
|
"spellchecker": "2.2.1",
|
||||||
"stacktrace-parser": "^0.1",
|
"stacktrace-parser": "^0.1",
|
||||||
"temp": "^0.8",
|
"temp": "^0.8",
|
||||||
"text-buffer": "^4.1",
|
"text-buffer": "^4.1",
|
||||||
"theorist": "^1.0",
|
"theorist": "^1.0",
|
||||||
"underscore": "^1.8",
|
"underscore": "^1.8",
|
||||||
"underscore.string": "^3.0",
|
"underscore.string": "^3.0",
|
||||||
"vm-compatibility-layer": "0.1.0",
|
"vm-compatibility-layer": "0.1.0"
|
||||||
"spellchecker": "2.2.1",
|
|
||||||
"aws-sdk": "2.1.28"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"proxyquire": "git+https://github.com/bengotow/proxyquire",
|
"proxyquire": "git+https://github.com/bengotow/proxyquire",
|
||||||
|
|
|
@ -10,6 +10,7 @@ SendDraftTask = require '../../src/flux/tasks/send-draft'
|
||||||
DestroyDraftTask = require '../../src/flux/tasks/destroy-draft'
|
DestroyDraftTask = require '../../src/flux/tasks/destroy-draft'
|
||||||
Actions = require '../../src/flux/actions'
|
Actions = require '../../src/flux/actions'
|
||||||
Utils = require '../../src/flux/models/utils'
|
Utils = require '../../src/flux/models/utils'
|
||||||
|
ipc = require 'ipc'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
|
|
||||||
fakeThread = null
|
fakeThread = null
|
||||||
|
@ -18,6 +19,7 @@ fakeMessage2 = null
|
||||||
msgFromMe = null
|
msgFromMe = null
|
||||||
msgWithReplyTo = null
|
msgWithReplyTo = null
|
||||||
msgWithReplyToDuplicates = null
|
msgWithReplyToDuplicates = null
|
||||||
|
messageWithStyleTags = null
|
||||||
fakeMessages = null
|
fakeMessages = null
|
||||||
|
|
||||||
class TestExtension extends DraftStoreExtension
|
class TestExtension extends DraftStoreExtension
|
||||||
|
@ -30,6 +32,18 @@ describe "DraftStore", ->
|
||||||
|
|
||||||
describe "creating drafts", ->
|
describe "creating drafts", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
spyOn(DraftStore, "_sanitizeBody").andCallThrough()
|
||||||
|
spyOn(DraftStore, "_onInlineStylesResult").andCallThrough()
|
||||||
|
spyOn(DraftStore, "_convertToInlineStyles").andCallThrough()
|
||||||
|
spyOn(ipc, "send").andCallFake (message, body) ->
|
||||||
|
if message is "inline-style-parse"
|
||||||
|
# There needs to be a defer block in here so the promise
|
||||||
|
# responsible for handling the `inline-style-parse` can be
|
||||||
|
# properly set. If the whole path is synchronous instead of
|
||||||
|
# asynchrounous, the promise is not cleared properly. Doing this
|
||||||
|
# requires us to add `advanceClock` blocks.
|
||||||
|
_.defer -> DraftStore._onInlineStylesResult(body)
|
||||||
|
|
||||||
fakeThread = new Thread
|
fakeThread = new Thread
|
||||||
id: 'fake-thread-id'
|
id: 'fake-thread-id'
|
||||||
subject: 'Fake Subject'
|
subject: 'Fake Subject'
|
||||||
|
@ -88,12 +102,24 @@ describe "DraftStore", ->
|
||||||
subject: 'Re: Fake Subject'
|
subject: 'Re: Fake Subject'
|
||||||
date: new Date(1415814587)
|
date: new Date(1415814587)
|
||||||
|
|
||||||
|
messageWithStyleTags = new Message
|
||||||
|
id: 'message-with-style-tags'
|
||||||
|
to: [new Contact(email: 'ben@nylas.com'), new Contact(email: 'evan@nylas.com')]
|
||||||
|
cc: [new Contact(email: 'mg@nylas.com'), new Contact(email: AccountStore.current().me().email)]
|
||||||
|
bcc: [new Contact(email: 'recruiting@nylas.com')]
|
||||||
|
from: [new Contact(email: 'customer@example.com', name: 'Customer')]
|
||||||
|
threadId: 'fake-thread-id'
|
||||||
|
body: '<style>div {color: red;}</style><div>Fake Message 1</div>'
|
||||||
|
subject: 'Fake Subject'
|
||||||
|
date: new Date(1415814587)
|
||||||
|
|
||||||
fakeMessages =
|
fakeMessages =
|
||||||
'fake-message-1': fakeMessage1
|
'fake-message-1': fakeMessage1
|
||||||
'fake-message-3': msgFromMe
|
'fake-message-3': msgFromMe
|
||||||
'fake-message-2': fakeMessage2
|
'fake-message-2': fakeMessage2
|
||||||
'fake-message-reply-to': msgWithReplyTo
|
'fake-message-reply-to': msgWithReplyTo
|
||||||
'fake-message-reply-to-duplicates': msgWithReplyToDuplicates
|
'fake-message-reply-to-duplicates': msgWithReplyToDuplicates
|
||||||
|
'message-with-style-tags': messageWithStyleTags
|
||||||
|
|
||||||
spyOn(DatabaseStore, 'find').andCallFake (klass, id) ->
|
spyOn(DatabaseStore, 'find').andCallFake (klass, id) ->
|
||||||
query = new ModelQuery(klass, {id})
|
query = new ModelQuery(klass, {id})
|
||||||
|
@ -133,6 +159,13 @@ describe "DraftStore", ->
|
||||||
it "should set the replyToMessageId to the previous message's ids", ->
|
it "should set the replyToMessageId to the previous message's ids", ->
|
||||||
expect(@model.replyToMessageId).toEqual(fakeMessage1.id)
|
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()
|
||||||
|
|
||||||
describe "onComposeReply", ->
|
describe "onComposeReply", ->
|
||||||
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
|
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
|
||||||
it "addresses the draft to all of the message's 'ReplyTo' recipients", ->
|
it "addresses the draft to all of the message's 'ReplyTo' recipients", ->
|
||||||
|
@ -190,6 +223,13 @@ describe "DraftStore", ->
|
||||||
it "should set the replyToMessageId to the previous message's ids", ->
|
it "should set the replyToMessageId to the previous message's ids", ->
|
||||||
expect(@model.replyToMessageId).toEqual(fakeMessage1.id)
|
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()
|
||||||
|
|
||||||
describe "onComposeReplyAll", ->
|
describe "onComposeReplyAll", ->
|
||||||
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
|
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
@ -256,6 +296,47 @@ describe "DraftStore", ->
|
||||||
it "should not set the replyToMessageId", ->
|
it "should not set the replyToMessageId", ->
|
||||||
expect(@model.replyToMessageId).toEqual(undefined)
|
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()
|
||||||
|
|
||||||
describe "_newMessageWithContext", ->
|
describe "_newMessageWithContext", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
# A helper method that makes it easy to test _newMessageWithContext, which
|
# A helper method that makes it easy to test _newMessageWithContext, which
|
||||||
|
|
|
@ -325,6 +325,12 @@ class Application
|
||||||
ipc.on 'from-react-remote-window', (event, json) =>
|
ipc.on 'from-react-remote-window', (event, json) =>
|
||||||
@windowManager.sendToMainWindow('from-react-remote-window', json)
|
@windowManager.sendToMainWindow('from-react-remote-window', json)
|
||||||
|
|
||||||
|
ipc.on 'inline-style-parse', (event, {body, clientId}) =>
|
||||||
|
juice = require 'juice'
|
||||||
|
body = juice(body)
|
||||||
|
# win = BrowserWindow.fromWebContents(event.sender)
|
||||||
|
event.sender.send('inline-styles-result', {body, clientId})
|
||||||
|
|
||||||
app.on 'activate-with-no-open-windows', (event) =>
|
app.on 'activate-with-no-open-windows', (event) =>
|
||||||
@windowManager.openWindowsForTokenState()
|
@windowManager.openWindowsForTokenState()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
897
src/chrome-user-agent-stylesheet-string.coffee
Normal file
897
src/chrome-user-agent-stylesheet-string.coffee
Normal file
|
@ -0,0 +1,897 @@
|
||||||
|
# This is the Chrome (Blink) default user-agent stylesheet. We need this
|
||||||
|
# when we use `automatic/juice` to inline CSS since emails will be
|
||||||
|
# assuming they're based off the default stylesheet instead of the Nylas
|
||||||
|
# stylesheet.
|
||||||
|
#
|
||||||
|
# From: https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css
|
||||||
|
module.exports = """
|
||||||
|
@namespace "http://www.w3.org/1999/xhtml";
|
||||||
|
html {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
head {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
meta {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
title {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
link {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
style {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
script {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: block;
|
||||||
|
margin: 8px
|
||||||
|
}
|
||||||
|
body:-webkit-full-page-media {
|
||||||
|
background-color: rgb(0, 0, 0)
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1__qem;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
layer {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
article, aside, footer, header, hgroup, main, nav, section {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
marquee {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
address {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
-webkit-margin-start: 40px;
|
||||||
|
-webkit-margin-end: 40px;
|
||||||
|
}
|
||||||
|
figcaption {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
figure {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-before: 1em;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
-webkit-margin-start: 40px;
|
||||||
|
-webkit-margin-end: 40px;
|
||||||
|
}
|
||||||
|
q {
|
||||||
|
display: inline
|
||||||
|
}
|
||||||
|
q:before {
|
||||||
|
content: open-quote;
|
||||||
|
}
|
||||||
|
q:after {
|
||||||
|
content: close-quote;
|
||||||
|
}
|
||||||
|
center {
|
||||||
|
display: block;
|
||||||
|
text-align: -webkit-center
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-before: 0.5em;
|
||||||
|
-webkit-margin-after: 0.5em;
|
||||||
|
-webkit-margin-start: auto;
|
||||||
|
-webkit-margin-end: auto;
|
||||||
|
border-style: inset;
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
|
map {
|
||||||
|
display: inline
|
||||||
|
}
|
||||||
|
video {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 2em;
|
||||||
|
-webkit-margin-before: 0.67__qem;
|
||||||
|
-webkit-margin-after: 0.67em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
:-webkit-any(article,aside,nav,section) h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
-webkit-margin-before: 0.83__qem;
|
||||||
|
-webkit-margin-after: 0.83em;
|
||||||
|
}
|
||||||
|
:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {
|
||||||
|
font-size: 1.17em;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
}
|
||||||
|
:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {
|
||||||
|
font-size: 1.00em;
|
||||||
|
-webkit-margin-before: 1.33__qem;
|
||||||
|
-webkit-margin-after: 1.33em;
|
||||||
|
}
|
||||||
|
:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {
|
||||||
|
font-size: .83em;
|
||||||
|
-webkit-margin-before: 1.67__qem;
|
||||||
|
-webkit-margin-after: 1.67em;
|
||||||
|
}
|
||||||
|
:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {
|
||||||
|
font-size: .67em;
|
||||||
|
-webkit-margin-before: 2.33__qem;
|
||||||
|
-webkit-margin-after: 2.33em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5em;
|
||||||
|
-webkit-margin-before: 0.83__qem;
|
||||||
|
-webkit-margin-after: 0.83em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.17em;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-before: 1.33__qem;
|
||||||
|
-webkit-margin-after: 1.33em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
display: block;
|
||||||
|
font-size: .83em;
|
||||||
|
-webkit-margin-before: 1.67__qem;
|
||||||
|
-webkit-margin-after: 1.67em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
display: block;
|
||||||
|
font-size: .67em;
|
||||||
|
-webkit-margin-before: 2.33__qem;
|
||||||
|
-webkit-margin-after: 2.33em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
display: table;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 2px;
|
||||||
|
border-color: gray
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
display: table-header-group;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: inherit
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
display: table-row-group;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: inherit
|
||||||
|
}
|
||||||
|
tfoot {
|
||||||
|
display: table-footer-group;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: inherit
|
||||||
|
}
|
||||||
|
table > tr {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
col {
|
||||||
|
display: table-column
|
||||||
|
}
|
||||||
|
colgroup {
|
||||||
|
display: table-column-group
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
display: table-row;
|
||||||
|
vertical-align: inherit;
|
||||||
|
border-color: inherit
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: inherit
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
caption {
|
||||||
|
display: table-caption;
|
||||||
|
text-align: -webkit-center
|
||||||
|
}
|
||||||
|
ul, menu, dir {
|
||||||
|
display: block;
|
||||||
|
list-style-type: disc;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
-webkit-padding-start: 40px
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
display: block;
|
||||||
|
list-style-type: decimal;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
-webkit-padding-start: 40px
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: list-item;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
ul ul, ol ul {
|
||||||
|
list-style-type: circle
|
||||||
|
}
|
||||||
|
ol ol ul, ol ul ul, ul ol ul, ul ul ul {
|
||||||
|
list-style-type: square
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-start: 40px
|
||||||
|
}
|
||||||
|
dl {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-before: 1__qem;
|
||||||
|
-webkit-margin-after: 1em;
|
||||||
|
-webkit-margin-start: 0;
|
||||||
|
-webkit-margin-end: 0;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
ol ul, ul ol, ul ul, ol ol {
|
||||||
|
-webkit-margin-before: 0;
|
||||||
|
-webkit-margin-after: 0
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0__qem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
-webkit-padding-start: 2px;
|
||||||
|
-webkit-padding-end: 2px;
|
||||||
|
border: none
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
display: block;
|
||||||
|
-webkit-margin-start: 2px;
|
||||||
|
-webkit-margin-end: 2px;
|
||||||
|
-webkit-padding-before: 0.35em;
|
||||||
|
-webkit-padding-start: 0.75em;
|
||||||
|
-webkit-padding-end: 0.75em;
|
||||||
|
-webkit-padding-after: 0.625em;
|
||||||
|
border: 2px groove ThreeDFace;
|
||||||
|
min-width: -webkit-min-content;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
input, textarea, keygen, select, button, meter, progress {
|
||||||
|
-webkit-writing-mode: horizontal-tb !important;
|
||||||
|
}
|
||||||
|
input, textarea, keygen, select, button {
|
||||||
|
margin: 0__qem;
|
||||||
|
font: -webkit-small-control;
|
||||||
|
text-rendering: auto;
|
||||||
|
color: initial;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-spacing: normal;
|
||||||
|
line-height: normal;
|
||||||
|
text-transform: none;
|
||||||
|
text-indent: 0;
|
||||||
|
text-shadow: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
input[type="hidden" i] {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
padding: 1px;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px inset;
|
||||||
|
-webkit-rtl-ordering: logical;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
input[type="search" i] {
|
||||||
|
-webkit-appearance: searchfield;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input::-webkit-textfield-decoration-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
input[type="search" i]::-webkit-textfield-decoration-container {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
input::-webkit-clear-button {
|
||||||
|
-webkit-appearance: searchfield-cancel-button;
|
||||||
|
display: inline-block;
|
||||||
|
flex: none;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
-webkit-margin-start: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
input:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-clear-button {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
input[type="search" i]::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: searchfield-cancel-button;
|
||||||
|
display: block;
|
||||||
|
flex: none;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
-webkit-margin-start: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
input[type="search" i]:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-search-cancel-button {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
input[type="search" i]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: searchfield-decoration;
|
||||||
|
display: block;
|
||||||
|
flex: none;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
-webkit-align-self: flex-start;
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
input[type="search" i]::-webkit-search-results-decoration {
|
||||||
|
-webkit-appearance: searchfield-results-decoration;
|
||||||
|
display: block;
|
||||||
|
flex: none;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
-webkit-align-self: flex-start;
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: inner-spin-button;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: default;
|
||||||
|
flex: none;
|
||||||
|
align-self: stretch;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
input:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-inner-spin-button {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
keygen, select {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
keygen::-webkit-keygen-select {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
-webkit-appearance: textarea;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid;
|
||||||
|
-webkit-rtl-ordering: logical;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
flex-direction: column;
|
||||||
|
resize: auto;
|
||||||
|
cursor: auto;
|
||||||
|
padding: 2px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
-webkit-text-security: none;
|
||||||
|
color: darkGray;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
input::-webkit-input-placeholder {
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
}
|
||||||
|
input[type="password" i] {
|
||||||
|
-webkit-text-security: disc !important;
|
||||||
|
}
|
||||||
|
input[type="hidden" i], input[type="image" i], input[type="file" i] {
|
||||||
|
-webkit-appearance: initial;
|
||||||
|
padding: initial;
|
||||||
|
background-color: initial;
|
||||||
|
border: initial;
|
||||||
|
}
|
||||||
|
input[type="file" i] {
|
||||||
|
align-items: baseline;
|
||||||
|
color: inherit;
|
||||||
|
text-align: start !important;
|
||||||
|
}
|
||||||
|
input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {
|
||||||
|
background-color: #FAFFBD !important;
|
||||||
|
background-image:none !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
input[type="radio" i], input[type="checkbox" i] {
|
||||||
|
margin: 3px 0.5ex;
|
||||||
|
padding: initial;
|
||||||
|
background-color: initial;
|
||||||
|
border: initial;
|
||||||
|
}
|
||||||
|
input[type="button" i], input[type="submit" i], input[type="reset" i] {
|
||||||
|
-webkit-appearance: push-button;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
white-space: pre
|
||||||
|
}
|
||||||
|
input[type="file" i]::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: push-button;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
input[type="button" i], input[type="submit" i], input[type="reset" i], input[type="file" i]::-webkit-file-upload-button, button {
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
color: ButtonText;
|
||||||
|
padding: 2px 6px 3px 6px;
|
||||||
|
border: 2px outset ButtonFace;
|
||||||
|
background-color: ButtonFace;
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
input[type="range" i] {
|
||||||
|
-webkit-appearance: slider-horizontal;
|
||||||
|
padding: initial;
|
||||||
|
border: initial;
|
||||||
|
margin: 2px;
|
||||||
|
color: #909090;
|
||||||
|
}
|
||||||
|
input[type="range" i]::-webkit-slider-container, input[type="range" i]::-webkit-media-slider-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
input[type="range" i]::-webkit-slider-runnable-track {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
-webkit-align-self: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
input[type="range" i]::-webkit-slider-thumb, input[type="range" i]::-webkit-media-slider-thumb {
|
||||||
|
-webkit-appearance: sliderthumb-horizontal;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
input[type="button" i]:disabled, input[type="submit" i]:disabled, input[type="reset" i]:disabled,
|
||||||
|
input[type="file" i]:disabled::-webkit-file-upload-button, button:disabled,
|
||||||
|
select:disabled, keygen:disabled, optgroup:disabled, option:disabled,
|
||||||
|
select[disabled]>option {
|
||||||
|
color: GrayText
|
||||||
|
}
|
||||||
|
input[type="button" i]:active, input[type="submit" i]:active, input[type="reset" i]:active, input[type="file" i]:active::-webkit-file-upload-button, button:active {
|
||||||
|
border-style: inset
|
||||||
|
}
|
||||||
|
input[type="button" i]:active:disabled, input[type="submit" i]:active:disabled, input[type="reset" i]:active:disabled, input[type="file" i]:active:disabled::-webkit-file-upload-button, button:active:disabled {
|
||||||
|
border-style: outset
|
||||||
|
}
|
||||||
|
option:-internal-spatial-navigation-focus {
|
||||||
|
outline: black dashed 1px;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
datalist {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
area {
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
param {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
input[type="checkbox" i] {
|
||||||
|
-webkit-appearance: checkbox;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input[type="radio" i] {
|
||||||
|
-webkit-appearance: radio;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input[type="color" i] {
|
||||||
|
-webkit-appearance: square-button;
|
||||||
|
width: 44px;
|
||||||
|
height: 23px;
|
||||||
|
background-color: ButtonFace;
|
||||||
|
border: 1px #a9a9a9 solid;
|
||||||
|
padding: 1px 2px;
|
||||||
|
}
|
||||||
|
input[type="color" i]::-webkit-color-swatch-wrapper {
|
||||||
|
display:flex;
|
||||||
|
padding: 4px 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
input[type="color" i]::-webkit-color-swatch {
|
||||||
|
background-color: #000000;
|
||||||
|
border: 1px solid #777777;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
}
|
||||||
|
input[type="color" i][list] {
|
||||||
|
-webkit-appearance: menulist;
|
||||||
|
width: 88px;
|
||||||
|
height: 23px
|
||||||
|
}
|
||||||
|
input[type="color" i][list]::-webkit-color-swatch-wrapper {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
input[type="color" i][list]::-webkit-color-swatch {
|
||||||
|
border-color: #000000;
|
||||||
|
}
|
||||||
|
input::-webkit-calendar-picker-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.66em;
|
||||||
|
height: 0.66em;
|
||||||
|
padding: 0.17em 0.34em;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
input::-webkit-calendar-picker-indicator:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
input:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-calendar-picker-indicator,
|
||||||
|
input::-webkit-calendar-picker-indicator:focus {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
input[type="date" i]:disabled::-webkit-clear-button,
|
||||||
|
input[type="date" i]:disabled::-webkit-inner-spin-button,
|
||||||
|
input[type="datetime-local" i]:disabled::-webkit-clear-button,
|
||||||
|
input[type="datetime-local" i]:disabled::-webkit-inner-spin-button,
|
||||||
|
input[type="month" i]:disabled::-webkit-clear-button,
|
||||||
|
input[type="month" i]:disabled::-webkit-inner-spin-button,
|
||||||
|
input[type="week" i]:disabled::-webkit-clear-button,
|
||||||
|
input[type="week" i]:disabled::-webkit-inner-spin-button,
|
||||||
|
input:disabled::-webkit-calendar-picker-indicator,
|
||||||
|
input[type="date" i][readonly]::-webkit-clear-button,
|
||||||
|
input[type="date" i][readonly]::-webkit-inner-spin-button,
|
||||||
|
input[type="datetime-local" i][readonly]::-webkit-clear-button,
|
||||||
|
input[type="datetime-local" i][readonly]::-webkit-inner-spin-button,
|
||||||
|
input[type="month" i][readonly]::-webkit-clear-button,
|
||||||
|
input[type="month" i][readonly]::-webkit-inner-spin-button,
|
||||||
|
input[type="week" i][readonly]::-webkit-clear-button,
|
||||||
|
input[type="week" i][readonly]::-webkit-inner-spin-button,
|
||||||
|
input[readonly]::-webkit-calendar-picker-indicator {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
-webkit-appearance: menulist;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid;
|
||||||
|
white-space: pre;
|
||||||
|
-webkit-rtl-ordering: logical;
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
select:not(:-internal-list-box) {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
select:-internal-list-box {
|
||||||
|
-webkit-appearance: listbox;
|
||||||
|
align-items: flex-start;
|
||||||
|
border: 1px inset gray;
|
||||||
|
border-radius: initial;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
optgroup {
|
||||||
|
font-weight: bolder;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
font-weight: normal;
|
||||||
|
display: block;
|
||||||
|
padding: 0 2px 1px 2px;
|
||||||
|
white-space: pre;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
select:-internal-list-box option,
|
||||||
|
select:-internal-list-box optgroup {
|
||||||
|
line-height: initial !important;
|
||||||
|
}
|
||||||
|
select:-internal-list-box:focus option:checked {
|
||||||
|
background-color: -internal-active-list-box-selection !important;
|
||||||
|
color: -internal-active-list-box-selection-text !important;
|
||||||
|
}
|
||||||
|
select:-internal-list-box option:checked {
|
||||||
|
background-color: -internal-inactive-list-box-selection !important;
|
||||||
|
color: -internal-inactive-list-box-selection-text !important;
|
||||||
|
}
|
||||||
|
select:-internal-list-box:disabled option:checked,
|
||||||
|
select:-internal-list-box option:checked:disabled {
|
||||||
|
color: gray !important;
|
||||||
|
}
|
||||||
|
select:-internal-list-box hr {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
output {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
meter {
|
||||||
|
-webkit-appearance: meter;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
width: 5em;
|
||||||
|
vertical-align: -0.2em;
|
||||||
|
}
|
||||||
|
meter::-webkit-meter-inner-element {
|
||||||
|
-webkit-appearance: inherit;
|
||||||
|
box-sizing: inherit;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
meter::-webkit-meter-bar {
|
||||||
|
background: linear-gradient(to bottom, #ddd, #eee 20%, #ccc 45%, #ccc 55%, #ddd);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
meter::-webkit-meter-optimum-value {
|
||||||
|
background: linear-gradient(to bottom, #ad7, #cea 20%, #7a3 45%, #7a3 55%, #ad7);
|
||||||
|
height: 100%;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
meter::-webkit-meter-suboptimum-value {
|
||||||
|
background: linear-gradient(to bottom, #fe7, #ffc 20%, #db3 45%, #db3 55%, #fe7);
|
||||||
|
height: 100%;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
meter::-webkit-meter-even-less-good-value {
|
||||||
|
background: linear-gradient(to bottom, #f77, #fcc 20%, #d44 45%, #d44 55%, #f77);
|
||||||
|
height: 100%;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
progress {
|
||||||
|
-webkit-appearance: progress-bar;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
width: 10em;
|
||||||
|
vertical-align: -0.2em;
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-inner-element {
|
||||||
|
-webkit-appearance: inherit;
|
||||||
|
box-sizing: inherit;
|
||||||
|
-webkit-user-modify: read-only;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
background-color: gray;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
background-color: green;
|
||||||
|
height: 100%;
|
||||||
|
width: 50%;
|
||||||
|
-webkit-user-modify: read-only !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
u, ins {
|
||||||
|
text-decoration: underline
|
||||||
|
}
|
||||||
|
strong, b {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
i, cite, em, var, address, dfn {
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
tt, code, kbd, samp {
|
||||||
|
font-family: monospace
|
||||||
|
}
|
||||||
|
pre, xmp, plaintext, listing {
|
||||||
|
display: block;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
margin: 1__qem 0
|
||||||
|
}
|
||||||
|
mark {
|
||||||
|
background-color: yellow;
|
||||||
|
color: black
|
||||||
|
}
|
||||||
|
big {
|
||||||
|
font-size: larger
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size: smaller
|
||||||
|
}
|
||||||
|
s, strike, del {
|
||||||
|
text-decoration: line-through
|
||||||
|
}
|
||||||
|
sub {
|
||||||
|
vertical-align: sub;
|
||||||
|
font-size: smaller
|
||||||
|
}
|
||||||
|
sup {
|
||||||
|
vertical-align: super;
|
||||||
|
font-size: smaller
|
||||||
|
}
|
||||||
|
nobr {
|
||||||
|
white-space: nowrap
|
||||||
|
}
|
||||||
|
:focus {
|
||||||
|
outline: auto 5px -webkit-focus-ring-color
|
||||||
|
}
|
||||||
|
html:focus, body:focus, input[readonly]:focus {
|
||||||
|
outline: none
|
||||||
|
}
|
||||||
|
applet:focus, embed:focus, iframe:focus, object:focus {
|
||||||
|
outline: none
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, keygen:focus, select:focus {
|
||||||
|
outline-offset: -2px
|
||||||
|
}
|
||||||
|
input[type="button" i]:focus,
|
||||||
|
input[type="checkbox" i]:focus,
|
||||||
|
input[type="file" i]:focus,
|
||||||
|
input[type="hidden" i]:focus,
|
||||||
|
input[type="image" i]:focus,
|
||||||
|
input[type="radio" i]:focus,
|
||||||
|
input[type="reset" i]:focus,
|
||||||
|
input[type="search" i]:focus,
|
||||||
|
input[type="submit" i]:focus,
|
||||||
|
input[type="file" i]:focus::-webkit-file-upload-button {
|
||||||
|
outline-offset: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
a:-webkit-any-link {
|
||||||
|
color: -webkit-link;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
a:-webkit-any-link:active {
|
||||||
|
color: -webkit-activelink
|
||||||
|
}
|
||||||
|
ruby, rt {
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
rt {
|
||||||
|
line-height: normal;
|
||||||
|
-webkit-text-emphasis: none;
|
||||||
|
}
|
||||||
|
ruby > rt {
|
||||||
|
display: block;
|
||||||
|
font-size: 50%;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
ruby > rp {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
noframes {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
frameset, frame {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
frameset {
|
||||||
|
border-color: inherit
|
||||||
|
}
|
||||||
|
iframe {
|
||||||
|
border: 2px inset
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.66em;
|
||||||
|
height: 0.66em;
|
||||||
|
-webkit-margin-end: 0.4em;
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
bdi, output {
|
||||||
|
unicode-bidi: -webkit-isolate;
|
||||||
|
}
|
||||||
|
bdo {
|
||||||
|
unicode-bidi: bidi-override;
|
||||||
|
}
|
||||||
|
textarea[dir=auto i] {
|
||||||
|
unicode-bidi: -webkit-plaintext;
|
||||||
|
}
|
||||||
|
dialog:not([open]) {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
dialog {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: -webkit-fit-content;
|
||||||
|
height: -webkit-fit-content;
|
||||||
|
margin: auto;
|
||||||
|
border: solid;
|
||||||
|
padding: 1em;
|
||||||
|
background: white;
|
||||||
|
color: black
|
||||||
|
}
|
||||||
|
dialog::backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0,0,0,0.1)
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: auto;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0px;
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
* { -webkit-columns: auto !important; }
|
||||||
|
}
|
||||||
|
"""
|
|
@ -125,4 +125,20 @@ DOMUtils =
|
||||||
|
|
||||||
return 0
|
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
|
module.exports = DOMUtils
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
moment = require 'moment'
|
|
||||||
ipc = require 'ipc'
|
ipc = require 'ipc'
|
||||||
|
crypto = require 'crypto'
|
||||||
|
moment = require 'moment'
|
||||||
|
sanitizeHtml = require 'sanitize-html'
|
||||||
|
|
||||||
DraftStoreProxy = require './draft-store-proxy'
|
DraftStoreProxy = require './draft-store-proxy'
|
||||||
DatabaseStore = require './database-store'
|
DatabaseStore = require './database-store'
|
||||||
|
@ -13,6 +15,7 @@ DestroyDraftTask = require '../tasks/destroy-draft'
|
||||||
Thread = require '../models/thread'
|
Thread = require '../models/thread'
|
||||||
Contact = require '../models/contact'
|
Contact = require '../models/contact'
|
||||||
Message = require '../models/message'
|
Message = require '../models/message'
|
||||||
|
Utils = require '../models/utils'
|
||||||
MessageUtils = require '../models/message-utils'
|
MessageUtils = require '../models/message-utils'
|
||||||
Actions = require '../actions'
|
Actions = require '../actions'
|
||||||
|
|
||||||
|
@ -22,6 +25,7 @@ TaskQueue = require './task-queue'
|
||||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||||
CoffeeHelpers = require '../coffee-helpers'
|
CoffeeHelpers = require '../coffee-helpers'
|
||||||
DOMUtils = require '../../dom-utils'
|
DOMUtils = require '../../dom-utils'
|
||||||
|
RegExpUtils = require '../../regexp-utils'
|
||||||
|
|
||||||
###
|
###
|
||||||
Public: DraftStore responds to Actions that interact with Drafts and exposes
|
Public: DraftStore responds to Actions that interact with Drafts and exposes
|
||||||
|
@ -65,6 +69,9 @@ class DraftStore
|
||||||
@_draftSessions = {}
|
@_draftSessions = {}
|
||||||
@_extensions = []
|
@_extensions = []
|
||||||
|
|
||||||
|
@_inlineStylePromises = {}
|
||||||
|
@_inlineStyleResolvers = {}
|
||||||
|
|
||||||
# We would ideally like to be able to calculate the sending state
|
# We would ideally like to be able to calculate the sending state
|
||||||
# declaratively from the existence of the SendDraftTask on the
|
# declaratively from the existence of the SendDraftTask on the
|
||||||
# TaskQueue.
|
# TaskQueue.
|
||||||
|
@ -83,6 +90,9 @@ class DraftStore
|
||||||
|
|
||||||
ipc.on 'mailto', @_onHandleMailtoLink
|
ipc.on 'mailto', @_onHandleMailtoLink
|
||||||
|
|
||||||
|
|
||||||
|
ipc.on 'inline-styles-result', @_onInlineStylesResult
|
||||||
|
|
||||||
# TODO: Doesn't work if we do window.addEventListener, but this is
|
# TODO: Doesn't work if we do window.addEventListener, but this is
|
||||||
# fragile. Pending an Atom fix perhaps?
|
# fragile. Pending an Atom fix perhaps?
|
||||||
|
|
||||||
|
@ -211,15 +221,27 @@ class DraftStore
|
||||||
DatabaseStore.persistModel(draft).then =>
|
DatabaseStore.persistModel(draft).then =>
|
||||||
Promise.resolve(draftClientId: draft.clientId)
|
Promise.resolve(draftClientId: draft.clientId)
|
||||||
|
|
||||||
_newMessageWithContext: ({thread, threadId, message, messageId, popout}, attributesCallback) =>
|
_newMessageWithContext: (args, attributesCallback) =>
|
||||||
return unless AccountStore.current()
|
return unless AccountStore.current()
|
||||||
|
|
||||||
# We accept all kinds of context. You can pass actual thread and message objects,
|
# We accept all kinds of context. You can pass actual thread and message objects,
|
||||||
# or you can pass Ids and we'll look them up. Passing the object is preferable,
|
# or you can pass Ids and we'll look them up. Passing the object is preferable,
|
||||||
# and in most cases "the data is right there" anyway. Lookups add extra latency
|
# and in most cases "the data is right there" anyway. Lookups add extra latency
|
||||||
# that feels bad.
|
# that feels bad.
|
||||||
queries = {}
|
queries = @_buildModelResolvers(args)
|
||||||
|
queries.attributesCallback = attributesCallback
|
||||||
|
|
||||||
|
# Waits for the query promises to resolve and then resolve with a hash
|
||||||
|
# of their resolved values. *swoon*
|
||||||
|
Promise.props(queries)
|
||||||
|
.then @_prepareNewMessageAttributes
|
||||||
|
.then @_constructDraft
|
||||||
|
.then @_finalizeAndPersistNewMessage
|
||||||
|
.then ({draftLocalId}) =>
|
||||||
|
Actions.composePopoutDraft(draftLocalId) if args.popout
|
||||||
|
|
||||||
|
_buildModelResolvers: ({thread, threadId, message, messageId}) ->
|
||||||
|
queries = {}
|
||||||
if thread?
|
if thread?
|
||||||
throw new Error("newMessageWithContext: `thread` present, expected a Model. Maybe you wanted to pass `threadId`?") unless thread instanceof Thread
|
throw new Error("newMessageWithContext: `thread` present, expected a Model. Maybe you wanted to pass `threadId`?") unless thread instanceof Thread
|
||||||
queries.thread = thread
|
queries.thread = thread
|
||||||
|
@ -235,77 +257,126 @@ class DraftStore
|
||||||
else
|
else
|
||||||
queries.message = DatabaseStore.findBy(Message, {threadId: threadId ? thread.id}).order(Message.attributes.date.descending()).limit(1)
|
queries.message = DatabaseStore.findBy(Message, {threadId: threadId ? thread.id}).order(Message.attributes.date.descending()).limit(1)
|
||||||
queries.message.include(Message.attributes.body)
|
queries.message.include(Message.attributes.body)
|
||||||
|
return queries
|
||||||
|
|
||||||
# Waits for the query promises to resolve and then resolve with a hash
|
_constructDraft: ({attributes, thread}) =>
|
||||||
# of their resolved values. *swoon*
|
return new Message _.extend {}, attributes,
|
||||||
Promise.props(queries).then ({thread, message}) =>
|
from: [AccountStore.current().me()]
|
||||||
attributes = attributesCallback(thread, message)
|
date: (new Date)
|
||||||
attributes.subject ?= subjectWithPrefix(thread.subject, 'Re:')
|
draft: true
|
||||||
attributes.body ?= ""
|
pristine: true
|
||||||
|
threadId: thread.id
|
||||||
|
accountId: thread.accountId
|
||||||
|
|
||||||
contactsAsHtml = (cs) ->
|
_prepareNewMessageAttributes: ({thread, message, attributesCallback}) =>
|
||||||
DOMUtils.escapeHTMLCharacters(_.invoke(cs, "toString").join(", "))
|
attributes = attributesCallback(thread, message)
|
||||||
|
attributes.subject ?= subjectWithPrefix(thread.subject, 'Re:')
|
||||||
|
|
||||||
|
# We set the clientID here so we have a unique id to use for shipping
|
||||||
|
# the body to the browser process.
|
||||||
|
attributes.clientId = Utils.generateTempId()
|
||||||
|
|
||||||
|
@_prepareAttributesBody(attributes).then (body) ->
|
||||||
|
attributes.body = body
|
||||||
|
|
||||||
if attributes.replyToMessage
|
if attributes.replyToMessage
|
||||||
replyToMessage = attributes.replyToMessage
|
msg = attributes.replyToMessage
|
||||||
|
attributes.subject = subjectWithPrefix(msg.subject, 'Re:')
|
||||||
|
attributes.replyToMessageId = msg.id
|
||||||
|
delete attributes.quotedMessage
|
||||||
|
|
||||||
attributes.subject = subjectWithPrefix(replyToMessage.subject, 'Re:')
|
else if attributes.forwardMessage
|
||||||
attributes.replyToMessageId = replyToMessage.id
|
msg = attributes.forwardMessage
|
||||||
attributes.body = """
|
|
||||||
|
if msg.files?.length > 0
|
||||||
|
attributes.files ?= []
|
||||||
|
attributes.files = attributes.files.concat(forwardMessage.files)
|
||||||
|
|
||||||
|
attributes.subject = subjectWithPrefix(msg.subject, 'Fwd:')
|
||||||
|
delete attributes.forwardedMessage
|
||||||
|
|
||||||
|
return {attributes, thread}
|
||||||
|
|
||||||
|
_prepareAttributesBody: (attributes) ->
|
||||||
|
if attributes.replyToMessage
|
||||||
|
replyToMessage = attributes.replyToMessage
|
||||||
|
@_prepareBodyForQuoting(replyToMessage.body, attributes.clientId).then (body) ->
|
||||||
|
return """
|
||||||
<br><br><blockquote class="gmail_quote"
|
<br><br><blockquote class="gmail_quote"
|
||||||
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||||
#{DOMUtils.escapeHTMLCharacters(replyToMessage.replyAttributionLine())}
|
#{DOMUtils.escapeHTMLCharacters(replyToMessage.replyAttributionLine())}
|
||||||
<br>
|
<br>
|
||||||
#{@_formatBodyForQuoting(replyToMessage.body)}
|
#{body}
|
||||||
</blockquote>"""
|
</blockquote>"""
|
||||||
delete attributes.quotedMessage
|
else if attributes.forwardMessage
|
||||||
|
forwardMessage = attributes.forwardMessage
|
||||||
if attributes.forwardMessage
|
contactsAsHtml = (cs) ->
|
||||||
forwardMessage = attributes.forwardMessage
|
DOMUtils.escapeHTMLCharacters(_.invoke(cs, "toString").join(", "))
|
||||||
fields = []
|
fields = []
|
||||||
fields.push("From: #{contactsAsHtml(forwardMessage.from)}") if forwardMessage.from.length > 0
|
fields.push("From: #{contactsAsHtml(forwardMessage.from)}") if forwardMessage.from.length > 0
|
||||||
fields.push("Subject: #{forwardMessage.subject}")
|
fields.push("Subject: #{forwardMessage.subject}")
|
||||||
fields.push("Date: #{forwardMessage.formattedDate()}")
|
fields.push("Date: #{forwardMessage.formattedDate()}")
|
||||||
fields.push("To: #{contactsAsHtml(forwardMessage.to)}") if forwardMessage.to.length > 0
|
fields.push("To: #{contactsAsHtml(forwardMessage.to)}") if forwardMessage.to.length > 0
|
||||||
fields.push("CC: #{contactsAsHtml(forwardMessage.cc)}") if forwardMessage.cc.length > 0
|
fields.push("CC: #{contactsAsHtml(forwardMessage.cc)}") if forwardMessage.cc.length > 0
|
||||||
fields.push("BCC: #{contactsAsHtml(forwardMessage.bcc)}") if forwardMessage.bcc.length > 0
|
fields.push("BCC: #{contactsAsHtml(forwardMessage.bcc)}") if forwardMessage.bcc.length > 0
|
||||||
|
@_prepareBodyForQuoting(forwardMessage.body, attributes.clientId).then (body) ->
|
||||||
if forwardMessage.files?.length > 0
|
return """
|
||||||
attributes.files ?= []
|
|
||||||
attributes.files = attributes.files.concat(forwardMessage.files)
|
|
||||||
|
|
||||||
attributes.subject = subjectWithPrefix(forwardMessage.subject, 'Fwd:')
|
|
||||||
attributes.body = """
|
|
||||||
<br><br><blockquote class="gmail_quote"
|
<br><br><blockquote class="gmail_quote"
|
||||||
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||||
Begin forwarded message:
|
Begin forwarded message:
|
||||||
<br><br>
|
<br><br>
|
||||||
#{fields.join('<br>')}
|
#{fields.join('<br>')}
|
||||||
<br><br>
|
<br><br>
|
||||||
#{@_formatBodyForQuoting(forwardMessage.body)}
|
#{body}
|
||||||
</blockquote>"""
|
</blockquote>"""
|
||||||
delete attributes.forwardedMessage
|
else return Promise.resolve("")
|
||||||
|
|
||||||
draft = new Message _.extend {}, attributes,
|
|
||||||
from: [AccountStore.current().me()]
|
|
||||||
date: (new Date)
|
|
||||||
draft: true
|
|
||||||
pristine: true
|
|
||||||
threadId: thread.id
|
|
||||||
accountId: thread.accountId
|
|
||||||
|
|
||||||
@_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
|
|
||||||
Actions.composePopoutDraft(draftClientId) if popout
|
|
||||||
|
|
||||||
|
|
||||||
# Eventually we'll want a nicer solution for inline attachments
|
# Eventually we'll want a nicer solution for inline attachments
|
||||||
_formatBodyForQuoting: (body="") =>
|
_prepareBodyForQuoting: (body="", clientId) =>
|
||||||
|
## Fix inline images
|
||||||
cidRE = MessageUtils.cidRegexString
|
cidRE = MessageUtils.cidRegexString
|
||||||
# Be sure to match over multiple lines with [\s\S]*
|
# Be sure to match over multiple lines with [\s\S]*
|
||||||
# Regex explanation here: https://regex101.com/r/vO6eN2/1
|
# Regex explanation here: https://regex101.com/r/vO6eN2/1
|
||||||
re = new RegExp("<img.*#{cidRE}[\\s\\S]*?>", "igm")
|
re = new RegExp("<img.*#{cidRE}[\\s\\S]*?>", "igm")
|
||||||
body.replace(re, "")
|
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
|
||||||
|
ipc.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: ({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' ]
|
||||||
|
|
||||||
_onPopoutBlankDraft: =>
|
_onPopoutBlankDraft: =>
|
||||||
account = AccountStore.current()
|
account = AccountStore.current()
|
||||||
return unless account
|
return unless account
|
||||||
|
|
|
@ -23,4 +23,6 @@ RegExpUtils =
|
||||||
# SO discussion: http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative/31991870#31991870
|
# SO discussion: http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative/31991870#31991870
|
||||||
hasValidSchemeRegex: -> new RegExp('^[a-z][a-z0-9+.-]*:', 'i')
|
hasValidSchemeRegex: -> new RegExp('^[a-z][a-z0-9+.-]*:', 'i')
|
||||||
|
|
||||||
|
looseStyleTag: -> /<style/gim
|
||||||
|
|
||||||
module.exports = RegExpUtils
|
module.exports = RegExpUtils
|
||||||
|
|
Loading…
Reference in a new issue