diff --git a/docs/ComposerExtensions.md b/docs/ComposerExtensions.md
index 7ef6d625e..0431b1a5b 100644
--- a/docs/ComposerExtensions.md
+++ b/docs/ComposerExtensions.md
@@ -35,12 +35,9 @@ class ProductsExtension extends ComposerExtension
return ["with the word '#{word}'?"]
return []
- @applyTransformsToDraft: ({draft}) ->
+ @applyTransformsForSending: ({draftBodyRootNode, draft}) ->
if @warningsForSending({draft})
- updated = draft.clone()
- updated.body += "
This email \
- contains competitor's product names \
- or trademarks used in context."
- return updated
- return draft
+ el = document.createElement('p');
+ el.innerText = "This email contains competitor's product names or trademarks used in context."
+ draftBodyRootNode.appendChild(el)
```
diff --git a/internal_packages/composer-emoji/lib/emoji-composer-extension.jsx b/internal_packages/composer-emoji/lib/emoji-composer-extension.jsx
index dc3ebc278..5541d85ec 100644
--- a/internal_packages/composer-emoji/lib/emoji-composer-extension.jsx
+++ b/internal_packages/composer-emoji/lib/emoji-composer-extension.jsx
@@ -133,20 +133,38 @@ class EmojiComposerExtension extends ComposerExtension {
return null;
};
- static applyTransformsToDraft = ({draft}) => {
- const nextDraft = draft.clone();
- nextDraft.body = nextDraft.body.replace(/
/g, (match, emojiName) =>
- emoji.get(emojiName)
- );
- return nextDraft;
+ static applyTransformsForSending = ({draftBodyRootNode}) => {
+ const imgs = draftBodyRootNode.querySelectorAll('img')
+ for (const imgEl of Array.from(imgs)) {
+ const names = imgEl.className.split(' ');
+ if (names[0] === 'emoji') {
+ const emojiChar = emoji.get(names[1]);
+ if (emojiChar) {
+ imgEl.parentNode.replaceChild(document.createTextNode(emojiChar), imgEl);
+ }
+ }
+ }
}
- static unapplyTransformsToDraft = ({draft}) => {
- const nextDraft = draft.clone();
- nextDraft.body = nextDraft.body.replace(RegExpUtils.emojiRegex(), (match) =>
- `
`
- );
- return nextDraft;
+ static unapplyTransformsForSending = ({draftBodyRootNode}) => {
+ const treeWalker = document.createTreeWalker(draftBodyRootNode, NodeFilter.SHOW_TEXT);
+ while (treeWalker.nextNode()) {
+ const textNode = treeWalker.currentNode;
+ const match = RegExpUtils.emojiRegex().exec(textNode.textContent);
+ if (match) {
+ const emojiPlusTrailingEl = textNode.splitText(match.index);
+ emojiPlusTrailingEl.splitText(match.length);
+ const emojiEl = emojiPlusTrailingEl;
+ const imgEl = document.createElement('img');
+ const emojiName = emoji.which(match[0])
+ imgEl.className = `emoji ${emojiName}`;
+ imgEl.src = EmojiStore.getImagePath(emojiName);
+ imgEl.width = '14';
+ imgEl.height = '14';
+ imgEl.style.marginTop = '-5px';
+ emojiEl.parentNode.replaceChild(imgEl, emojiEl);
+ }
+ }
}
static _findEmojiOptions(sel) {
diff --git a/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee b/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee
index f08f00a4a..27a02bb09 100644
--- a/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee
+++ b/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee
@@ -2,20 +2,16 @@ marked = require 'marked'
Utils = require './utils'
{ComposerExtension} = require 'nylas-exports'
-
rawBodies = {}
class MarkdownComposerExtension extends ComposerExtension
- @applyTransformsToDraft: ({draft}) ->
- nextDraft = draft.clone()
- rawBodies[draft.clientId] = nextDraft.body
- nextDraft.body = marked(Utils.getTextFromHtml(draft.body))
- return nextDraft
+ @applyTransformsForSending: ({draftBodyRootNode, draft}) ->
+ rawBodies[draft.clientId] = draftBodyRootNode.innerHTML
+ draftBodyRootNode.innerHTML = marked(draftBodyRootNode.innerText)
- @unapplyTransformsToDraft: ({draft}) ->
- nextDraft = draft.clone()
- nextDraft.body = rawBodies[nextDraft.clientId] ? nextDraft.body
- return nextDraft
+ @unapplyTransformsForSending: ({draftBodyRootNode, draft}) ->
+ if rawBodies[draft.clientId]
+ draftBodyRootNode.innerHTML = rawBodies[draft.clientId]
module.exports = MarkdownComposerExtension
diff --git a/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6 b/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6
index 197bf9dc4..1dc7b553e 100644
--- a/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6
+++ b/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6
@@ -163,11 +163,19 @@ export default class SpellcheckComposerExtension extends ComposerExtension {
});
}
- static applyTransformsToDraft = ({draft}) => {
- const nextDraft = draft.clone();
- nextDraft.body = nextDraft.body.replace(/<\/?spelling[^>]*>/g, '');
- return nextDraft;
+ static applyTransformsForSending = ({draftBodyRootNode}) => {
+ const spellingEls = draftBodyRootNode.querySelectorAll('spelling');
+ for (const spellingEl of Array.from(spellingEls)) {
+ // move contents out of the spelling node, remove the node
+ const parent = spellingEl.parentNode;
+ while (spellingEl.firstChild) {
+ parent.insertBefore(spellingEl.firstChild, spellingEl);
+ }
+ parent.removeChild(spellingEl);
+ }
}
- static unapplyTransformsToDraft = () => 'unnecessary'
+ static unapplyTransformsForSending = () => {
+ // no need to put spelling nodes back!
+ }
}
diff --git a/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6 b/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6
index 79041afcb..29bfd8a1c 100644
--- a/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6
+++ b/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6
@@ -8,8 +8,8 @@ import {NylasSpellchecker, Message} from 'nylas-exports';
const initialPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-before.html');
const initialHTML = fs.readFileSync(initialPath).toString();
-const expectedPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-after.html');
-const expectedHTML = fs.readFileSync(expectedPath).toString();
+const afterPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-after.html');
+const afterHTML = fs.readFileSync(afterPath).toString();
describe('SpellcheckComposerExtension', function spellcheckComposerExtension() {
beforeEach(() => {
@@ -30,23 +30,19 @@ describe('SpellcheckComposerExtension', function spellcheckComposerExtension() {
};
SpellcheckComposerExtension.update(editor);
- expect(node.innerHTML).toEqual(expectedHTML);
+ expect(node.innerHTML).toEqual(afterHTML);
});
});
- describe("applyTransformsToDraft", () => {
+ describe("applyTransformsForSending", () => {
it("removes the spelling annotations it inserted", () => {
- const draft = new Message({ body: expectedHTML });
- const out = SpellcheckComposerExtension.applyTransformsToDraft({draft});
- expect(out.body).toEqual(initialHTML);
- });
- });
-
- describe("unapplyTransformsToDraft", () => {
- it("returns the magic no-op option", () => {
- const draft = new Message({ body: expectedHTML });
- const out = SpellcheckComposerExtension.unapplyTransformsToDraft({draft});
- expect(out).toEqual('unnecessary');
+ const draft = new Message({ body: afterHTML });
+ const fragment = document.createDocumentFragment();
+ const draftBodyRootNode = document.createElement('root')
+ fragment.appendChild(draftBodyRootNode)
+ draftBodyRootNode.innerHTML = afterHTML
+ SpellcheckComposerExtension.applyTransformsForSending({draftBodyRootNode, draft});
+ expect(draftBodyRootNode.innerHTML).toEqual(initialHTML);
});
});
});
diff --git a/internal_packages/composer-templates/lib/template-composer-extension.es6 b/internal_packages/composer-templates/lib/template-composer-extension.es6
index fad672444..897ca8dbc 100644
--- a/internal_packages/composer-templates/lib/template-composer-extension.es6
+++ b/internal_packages/composer-templates/lib/template-composer-extension.es6
@@ -10,20 +10,16 @@ export default class TemplatesComposerExtension extends ComposerExtension {
return warnings;
}
- static applyTransformsToDraft = ({draft}) => {
- const nextDraft = draft.clone();
- nextDraft.body = nextDraft.body.replace(/<\/?code[^>]*>/g, (match) =>
+ static applyTransformsForSending = ({draftBodyRootNode}) => {
+ draftBodyRootNode.innerHTML = draftBodyRootNode.innerHTML.replace(/<\/?code[^>]*>/g, (match) =>
``
);
- return nextDraft;
}
- static unapplyTransformsToDraft = ({draft}) => {
- const nextDraft = draft.clone();
- nextDraft.body = nextDraft.body.replace(//g, (match, node) =>
+ static unapplyTransformsForSending = ({draftBodyRootNode}) => {
+ draftBodyRootNode.innerHTML = draftBodyRootNode.innerHTML.replace(//g, (match, node) =>
node
);
- return nextDraft;
}
static onClick({editor, event}) {
diff --git a/internal_packages/composer/spec/composer-view-spec.cjsx b/internal_packages/composer/spec/composer-view-spec.cjsx
index cebd6b6aa..7a3a12778 100644
--- a/internal_packages/composer/spec/composer-view-spec.cjsx
+++ b/internal_packages/composer/spec/composer-view-spec.cjsx
@@ -180,7 +180,7 @@ describe "ComposerView", ->
describe "empty body warning", ->
it "warns if the body of the email is still the pristine body", ->
- pristineBody = "
"
+ pristineBody = "
"
useDraft.call @,
to: [u1]
diff --git a/spec/stores/draft-helpers-spec.es6 b/spec/stores/draft-helpers-spec.es6
index ff21bf529..585298d99 100644
--- a/spec/stores/draft-helpers-spec.es6
+++ b/spec/stores/draft-helpers-spec.es6
@@ -8,7 +8,7 @@ import {
describe('DraftHelpers', function describeBlock() {
describe('prepareDraftForSyncback', () => {
beforeEach(() => {
- spyOn(DraftHelpers, 'applyExtensionTransformsToDraft').andCallFake((draft) => Promise.resolve(draft))
+ spyOn(DraftHelpers, 'applyExtensionTransforms').andCallFake((draft) => Promise.resolve(draft))
spyOn(Actions, 'queueTask')
});
diff --git a/src/components/overlaid-components/overlaid-composer-extension.es6 b/src/components/overlaid-components/overlaid-composer-extension.es6
index e32c3b8a2..13f3f235c 100644
--- a/src/components/overlaid-components/overlaid-composer-extension.es6
+++ b/src/components/overlaid-components/overlaid-composer-extension.es6
@@ -1,88 +1,41 @@
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import ComposerExtension from '../../extensions/composer-extension'
-// import {ANCHOR_CLASS, IMG_SRC} from './anchor-constants'
import OverlaidComponents from './overlaid-components'
import CustomContenteditableComponents from './custom-contenteditable-components'
+// In this code, "anchor" refers to the "img" tag used in the draft while the
+// user is editing it.
+
+// is used when the draft is sent.
export default class OverlaidComposerExtension extends ComposerExtension {
- // https://regex101.com/r/fW6sV3/2
- static _serializedExtractRe() {
- return /.*?<\/overlay>/gmi
- }
-
- static _serializedReplacerRe(id) {
- return new RegExp(`.*?<\/overlay>`, 'gim')
- }
-
- // https://regex101.com/r/rK3uA3/1
- static _anchorExtractRe() {
- return /
/gmi
- }
-
- static _anchorReplacerRe(id) {
- return new RegExp(`
`, 'gim')
- }
-
- static *overlayMatches(re, body) {
- let result = re.exec(body);
- while (result) {
- let props = result[2];
- props = JSON.parse(props.replace(/"/g, `"`));
- const data = {
- dataOverlayId: result[1],
- dataComponentProps: props,
- dataComponentKey: result[3],
- dataStyle: result[4],
+ static applyTransformsForSending({draftBodyRootNode, draft}) {
+ const overlayImgEls = Array.from(draftBodyRootNode.querySelectorAll('img[data-overlay-id]'));
+ for (const imgEl of overlayImgEls) {
+ const Component = CustomContenteditableComponents.get(imgEl.dataset.componentKey);
+ if (!Component) {
+ continue;
}
- yield data
- result = re.exec(body);
+
+ const props = Object.assign({draft, isPreview: true}, imgEl.dataset.componentProps);
+ const reactElement = React.createElement(Component, props);
+
+ const overlayEl = document.createElement('overlay');
+ overlayEl.innerHTML = ReactDOMServer.renderToStaticMarkup(reactElement);
+ Object.assign(overlayEl.dataset, imgEl.dataset);
+
+ imgEl.parentNode.replaceChild(overlayEl, imgEl);
}
- return
}
- static applyTransformsToDraft({draft}) {
- const self = OverlaidComposerExtension;
- const outDraft = draft.clone();
- let outBody = outDraft.body;
- const matcher = self.overlayMatches(self._anchorExtractRe(), outDraft.body)
-
- for (const match of matcher) {
- const component = CustomContenteditableComponents.get(match.dataComponentKey);
- if (!component) {
- continue
- }
- const props = Object.assign({draft, isPreview: true}, match.dataComponentProps);
- const el = React.createElement(component, props);
- let html = ReactDOMServer.renderToStaticMarkup(el);
-
- html = `${html}`
-
- outBody = outBody.replace(
- OverlaidComposerExtension._anchorReplacerRe(match.dataOverlayId),
- html
- )
+ static unapplyTransformsForSending({draftBodyRootNode}) {
+ const overlayEls = Array.from(draftBodyRootNode.querySelectorAll('overlay[data-overlay-id]'));
+ for (const overlayEl of overlayEls) {
+ const {componentKey, componentProps, overlayId, style} = overlayEl.dataset;
+ const {anchorTag} = OverlaidComponents.buildAnchorTag(componentKey, componentProps, overlayId, style);
+ const anchorFragment = document.createRange().createContextualFragment(anchorTag);
+ overlayEl.parentNode.replaceChild(anchorFragment, overlayEl);
}
-
- outDraft.body = outBody;
- return outDraft;
- }
-
- static unapplyTransformsToDraft({draft}) {
- const self = OverlaidComposerExtension;
- const outDraft = draft.clone();
- let outBody = outDraft.body
-
- const matcher = self.overlayMatches(self._serializedExtractRe(), outDraft.body);
-
- for (const match of matcher) {
- const {anchorTag} = OverlaidComponents.buildAnchorTag(match.dataComponentKey, match.dataComponentProps, match.dataOverlayId, match.dataStyle);
-
- outBody = outBody.replace(OverlaidComposerExtension._serializedReplacerRe(match.dataOverlayId), anchorTag)
- }
-
- outDraft.body = outBody;
- return outDraft;
}
}
diff --git a/src/extensions/composer-extension.coffee b/src/extensions/composer-extension.coffee
index 7af5b0c92..a13e0a2f1 100644
--- a/src/extensions/composer-extension.coffee
+++ b/src/extensions/composer-extension.coffee
@@ -142,15 +142,15 @@ class ComposerExtension extends ContenteditableExtension
- `draft`: A {Message} the user is about to finish editing.
###
- @applyTransformsToDraft: ({draft}) ->
- return draft
+ @applyTransformsForSending: ({draft, draftBodyRootNode}) ->
+ return
###
Public: unapplyTransformsToDraft should revert the changes made in
`applyTransformsToDraft`. See the documentation for that method for more
information.
###
- @unapplyTransformsToDraft: ({draft}) ->
- return draft
+ @unapplyTransformsForSending: ({draft, draftBodyRootNode}) ->
+ return
module.exports = ComposerExtension
diff --git a/src/flux/stores/draft-editing-session.coffee b/src/flux/stores/draft-editing-session.coffee
index f4caef0ee..59ae7d049 100644
--- a/src/flux/stores/draft-editing-session.coffee
+++ b/src/flux/stores/draft-editing-session.coffee
@@ -209,14 +209,21 @@ class DraftEditingSession
if !draft.body?
throw new Error("DraftEditingSession._setDraft - new draft has no body!")
- # Reverse draft transformations performed by third-party plugins when the draft
- # was last saved to disk
- return Promise.each ExtensionRegistry.Composer.extensions(), (ext) ->
- if ext.applyTransformsToDraft and ext.unapplyTransformsToDraft
- Promise.resolve(ext.unapplyTransformsToDraft({draft})).then (untransformed) ->
- unless untransformed is 'unnecessary'
- draft = untransformed
+ extensions = ExtensionRegistry.Composer.extensions()
+
+ # Run `extensions[].unapplyTransformsForSending`
+ fragment = document.createDocumentFragment()
+ draftBodyRootNode = document.createElement('root')
+ fragment.appendChild(draftBodyRootNode)
+ draftBodyRootNode.innerHTML = draft.body
+
+ return Promise.each extensions, (ext) ->
+ if ext.applyTransformsForSending and ext.unapplyTransformsForSending
+ Promise.resolve(ext.unapplyTransformsForSending({
+ draftBodyRootNode: draftBodyRootNode,
+ draft: draft}))
.then =>
+ draft.body = draftBodyRootNode.innerHTML
@_draft = draft
# We keep track of the draft's initial body if it's pristine when the editing
diff --git a/src/flux/stores/draft-helpers.es6 b/src/flux/stores/draft-helpers.es6
index 80f888224..e298b33b0 100644
--- a/src/flux/stores/draft-helpers.es6
+++ b/src/flux/stores/draft-helpers.es6
@@ -1,4 +1,3 @@
-import _ from 'underscore'
import Actions from '../actions'
import DatabaseStore from './database-store'
import Message from '../models/message'
@@ -75,56 +74,48 @@ export function appendQuotedTextToDraft(draft) {
})
}
-export function applyExtensionTransformsToDraft(draft) {
- let latestTransformed = draft
- const extensions = ExtensionRegistry.Composer.extensions()
- const transformPromise = (
- Promise.each(extensions, (ext) => {
- const extApply = ext.applyTransformsToDraft
- const extUnapply = ext.unapplyTransformsToDraft
+export function applyExtensionTransforms(draft) {
+ const extensions = ExtensionRegistry.Composer.extensions();
- if (!extApply || !extUnapply) {
- return Promise.resolve()
- }
+ const fragment = document.createDocumentFragment();
+ const draftBodyRootNode = document.createElement('root');
+ fragment.appendChild(draftBodyRootNode);
+ draftBodyRootNode.innerHTML = draft.body;
- return Promise.resolve(extUnapply({draft: latestTransformed})).then((cleaned) => {
- const base = cleaned === 'unnecessary' ? latestTransformed : cleaned;
- return Promise.resolve(extApply({draft: base})).then((transformed) => (
- Promise.resolve(extUnapply({draft: transformed.clone()})).then((reverted) => {
- const untransformed = reverted === 'unnecessary' ? base : reverted;
- if (!_.isEqual(_.pick(untransformed, AllowedTransformFields), _.pick(base, AllowedTransformFields))) {
- console.log("-- BEFORE --")
- console.log(base.body)
- console.log("-- TRANSFORMED --")
- console.log(transformed.body)
- console.log("-- UNTRANSFORMED (should match BEFORE) --")
- console.log(untransformed.body)
- // FIXME: We're removing the error reporting for now, but the real fix is finding out why the console opens when dev mode is false.
- // NylasEnv.reportError(new Error(`Extension ${ext.name} applied a transform to the draft that it could not reverse.`))
- }
- latestTransformed = transformed
- return Promise.resolve()
- })
- ))
- })
- })
- )
- return transformPromise
- .then(() => Promise.resolve(latestTransformed))
+ return Promise.each(extensions, (ext) => {
+ const extApply = ext.applyTransformsForSending;
+ const extUnapply = ext.unapplyTransformsForSending;
+
+ if (!extApply || !extUnapply) {
+ return Promise.resolve();
+ }
+
+ return Promise.resolve(extUnapply({draft, draftBodyRootNode})).then(() => {
+ return Promise.resolve(extApply({draft, draftBodyRootNode}));
+ });
+ }).then(() => {
+ draft.body = draftBodyRootNode.innerHTML;
+ return draft;
+ });
}
export function prepareDraftForSyncback(session) {
return session.ensureCorrectAccount({noSyncback: true})
- .then(() => applyExtensionTransformsToDraft(session.draft()))
+ .then(() =>
+ applyExtensionTransforms(session.draft()))
.then((transformed) => {
if (!transformed.replyToMessageId || !shouldAppendQuotedText(transformed)) {
- return Promise.resolve(transformed)
+ return Promise.resolve(transformed);
}
- return appendQuotedTextToDraft(transformed)
+ return appendQuotedTextToDraft(transformed);
})
.then((draft) => (
- DatabaseStore.inTransaction((t) => t.persistModel(draft))
- .then(() => Promise.resolve(queueDraftFileUploads(draft)))
+ DatabaseStore.inTransaction((t) =>
+ t.persistModel(draft)
+ )
+ .then(() =>
+ Promise.resolve(queueDraftFileUploads(draft))
+ )
.thenReturn(draft)
))
}
diff --git a/src/pro b/src/pro
index 45756b806..a5d7c9c6c 160000
--- a/src/pro
+++ b/src/pro
@@ -1 +1 @@
-Subproject commit 45756b80645edcedfa7d015dcff8a26bcb521ec0
+Subproject commit a5d7c9c6c1b13559d1bb997eece8c7a5f648ea5c