From c76582194a1c04ca0b0b24c0ff8e87775f1f9390 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Mon, 14 Mar 2016 12:30:54 -0700 Subject: [PATCH] rm(autolinker): Use our own very simple autolinker Summary: Autolinker is a great open source project but it attempts to parse HTML with regexp, is quite slow, and hangs on specific emails https://github.com/nylas/N1/issues/1540 This is super bad, and also super unnecessary. I think this should do the trick. Note: I changed the urlRegex in our Utils to be much more liberal. It now matches anything that looks like a URL, not just things with the http:// and https:// prefixes. It's used in the LinkEditor and onboarding screen (detecting auth errors with urls) and I think it should be ok? Test Plan: Need to write some tests Reviewers: evan, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2725 --- build/config/eslint.json | 1 + .../message-list/lib/autolinker.es6 | 54 + .../message-list/lib/email-frame.cjsx | 89 -- .../message-list/lib/email-frame.jsx | 119 ++ internal_packages/message-list/lib/main.cjsx | 3 - .../lib/plugins/autolinker-extension.coffee | 31 - internal_packages/message-list/package.json | 3 - .../spec/autolinker-extension-spec.coffee | 28 - .../spec/autolinker-fixtures/gmail-in.html | 184 +++ .../spec/autolinker-fixtures/gmail-out.html | 153 ++ .../spec/autolinker-fixtures/linkedin-in.html | 1248 +++++++++++++++++ .../autolinker-fixtures/linkedin-out.html | 1010 +++++++++++++ .../autolinker-fixtures/plaintext-in.html | 26 + .../autolinker-fixtures/plaintext-out.html | 28 + .../message-list/spec/autolinker-spec.es6 | 24 + src/regexp-utils.coffee | 12 +- 16 files changed, 2853 insertions(+), 160 deletions(-) create mode 100644 internal_packages/message-list/lib/autolinker.es6 delete mode 100644 internal_packages/message-list/lib/email-frame.cjsx create mode 100644 internal_packages/message-list/lib/email-frame.jsx delete mode 100644 internal_packages/message-list/lib/plugins/autolinker-extension.coffee delete mode 100644 internal_packages/message-list/spec/autolinker-extension-spec.coffee create mode 100644 internal_packages/message-list/spec/autolinker-fixtures/gmail-in.html create mode 100644 internal_packages/message-list/spec/autolinker-fixtures/gmail-out.html create mode 100644 internal_packages/message-list/spec/autolinker-fixtures/linkedin-in.html create mode 100644 internal_packages/message-list/spec/autolinker-fixtures/linkedin-out.html create mode 100644 internal_packages/message-list/spec/autolinker-fixtures/plaintext-in.html create mode 100644 internal_packages/message-list/spec/autolinker-fixtures/plaintext-out.html create mode 100644 internal_packages/message-list/spec/autolinker-spec.es6 diff --git a/build/config/eslint.json b/build/config/eslint.json index 54f00c671..3c5e7cea9 100644 --- a/build/config/eslint.json +++ b/build/config/eslint.json @@ -13,6 +13,7 @@ "rules": { "react/prop-types": [2, {"ignore": ["children"]}], "react/no-multi-comp": [0], + "react/sort-comp": [2], "eqeqeq": [2, "smart"], "id-length": [0], "object-curly-spacing": [0], diff --git a/internal_packages/message-list/lib/autolinker.es6 b/internal_packages/message-list/lib/autolinker.es6 new file mode 100644 index 000000000..0bf64d1ae --- /dev/null +++ b/internal_packages/message-list/lib/autolinker.es6 @@ -0,0 +1,54 @@ +import {RegExpUtils, DOMUtils} from 'nylas-exports'; + +function _runOnTextNode(node, matchers) { + if (node.parentElement) { + const withinScript = node.parentElement.tagName === "SCRIPT"; + const withinStyle = node.parentElement.tagName === "STYLE"; + const withinA = (node.parentElement.closest('a') !== null); + if (withinScript || withinA || withinStyle) { + return; + } + } + if (node.textContent.trim().length < 4) { + return; + } + for (const [prefix, regex] of matchers) { + regex.lastIndex = 0; + const match = regex.exec(node.textContent); + if (match !== null) { + const href = `${prefix}${match[0]}`; + const range = document.createRange(); + range.setStart(node, match.index); + range.setEnd(node, match.index + match[0].length); + const aTag = DOMUtils.wrap(range, 'A'); + aTag.href = href; + aTag.title = href; + return; + } + } +} + +export function autolink(doc) { + // Traverse the new DOM tree and make things that look like links clickable, + // and ensure anything with an href has a title attribute. + const textWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT); + const matchers = [ + ['mailto:', RegExpUtils.emailRegex()], + ['tel:', RegExpUtils.phoneRegex()], + ['', RegExpUtils.urlRegex({matchEntireString: false})], + ]; + + while (textWalker.nextNode()) { + _runOnTextNode(textWalker.currentNode, matchers); + } + + // Traverse the new DOM tree and make sure everything with an href has a title. + const aTagWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, { + acceptNode: (node) => + (node.href && !node.title) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP + , + }); + while (aTagWalker.nextNode()) { + aTagWalker.currentNode.title = aTagWalker.currentNode.href; + } +} diff --git a/internal_packages/message-list/lib/email-frame.cjsx b/internal_packages/message-list/lib/email-frame.cjsx deleted file mode 100644 index deecf0de6..000000000 --- a/internal_packages/message-list/lib/email-frame.cjsx +++ /dev/null @@ -1,89 +0,0 @@ -React = require 'react' -_ = require "underscore" -{EventedIFrame} = require 'nylas-component-kit' -{Utils, QuotedHTMLTransformer} = require 'nylas-exports' - -EmailFrameStylesStore = require './email-frame-styles-store' - -class EmailFrame extends React.Component - @displayName = 'EmailFrame' - - @propTypes: - content: React.PropTypes.string.isRequired - - render: => - - - componentDidMount: => - @_mounted = true - @_writeContent() - @_unlisten = EmailFrameStylesStore.listen(@_writeContent) - - componentWillUnmount: => - @_mounted = false - @_unlisten?() - - componentDidUpdate: => - @_writeContent() - - shouldComponentUpdate: (nextProps, nextState) => - not Utils.isEqualReact(nextProps, @props) or - not Utils.isEqualReact(nextState, @state) - - _writeContent: => - @_lastComputedHeight = 0 - domNode = React.findDOMNode(@) - doc = domNode.contentDocument - return unless doc - - doc.open() - - # NOTE: The iframe must have a modern DOCTYPE. The lack of this line - # will cause some bizzare non-standards compliant rendering with the - # message bodies. This is particularly felt with elements use - # the `border-collapse: collapse` css property while setting a - # `padding`. - doc.write("") - styles = EmailFrameStylesStore.styles() - if (styles) - doc.write("") - doc.write("
#{@_emailContent()}
") - doc.close() - - # Notify the EventedIFrame that we've replaced it's document (with `open`) - # so it can attach event listeners again. - @refs.iframe.documentWasReplaced() - domNode.height = '0px' - @_setFrameHeight() - - _getFrameHeight: (doc) -> - return 0 unless doc - return doc.body?.scrollHeight ? doc.documentElement?.scrollHeight ? 0 - - _setFrameHeight: => - return unless @_mounted - - domNode = React.findDOMNode(@) - height = @_getFrameHeight(domNode.contentDocument) - - # Why 5px? Some emails have elements with a height of 100%, and then put - # tracking pixels beneath that. In these scenarios, the scrollHeight of the - # message is always <100% + 1px>, which leads us to resize them constantly. - # This is a hack, but I'm not sure of a better solution. - if Math.abs(height - @_lastComputedHeight) > 5 - domNode.height = "#{height}px" - @_lastComputedHeight = height - - unless domNode.contentDocument?.readyState is 'complete' - _.defer => @_setFrameHeight() - - _emailContent: => - # When showing quoted text, always return the pure content - if @props.showQuotedText - @props.content - else - QuotedHTMLTransformer.removeQuotedHTML(@props.content, keepIfWholeBodyIsQuote: true) - - -module.exports = EmailFrame diff --git a/internal_packages/message-list/lib/email-frame.jsx b/internal_packages/message-list/lib/email-frame.jsx new file mode 100644 index 000000000..f43b3df1d --- /dev/null +++ b/internal_packages/message-list/lib/email-frame.jsx @@ -0,0 +1,119 @@ +import React from 'react'; +import _ from "underscore"; +import {EventedIFrame} from 'nylas-component-kit'; +import {Utils, QuotedHTMLTransformer} from 'nylas-exports'; +import {autolink} from './autolinker'; +import EmailFrameStylesStore from './email-frame-styles-store'; + +export default class EmailFrame extends React.Component { + static displayName = 'EmailFrame'; + + static propTypes = { + content: React.PropTypes.string.isRequired, + showQuotedText: React.PropTypes.bool, + }; + + componentDidMount() { + this._mounted = true; + this._writeContent(); + this._unlisten = EmailFrameStylesStore.listen(this._writeContent); + } + + shouldComponentUpdate(nextProps, nextState) { + return (!Utils.isEqualReact(nextProps, this.props) || + !Utils.isEqualReact(nextState, this.state)); + } + + componentDidUpdate() { + this._writeContent(); + } + + componentWillUnmount() { + this._mounted = false; + if (this._unlisten) { + this._unlisten(); + } + } + + _emailContent = () => { + // When showing quoted text, always return the pure content + if (this.props.showQuotedText) { + return this.props.content; + } + return QuotedHTMLTransformer.removeQuotedHTML(this.props.content, { + keepIfWholeBodyIsQuote: true, + }); + } + + _writeContent = () => { + this._lastComputedHeight = 0; + const domNode = React.findDOMNode(this); + const doc = domNode.contentDocument; + if (!doc) { return; } + doc.open(); + + // NOTE: The iframe must have a modern DOCTYPE. The lack of this line + // will cause some bizzare non-standards compliant rendering with the + // message bodies. This is particularly felt with
elements use + // the `border-collapse: collapse` css property while setting a + // `padding`. + doc.write(""); + const styles = EmailFrameStylesStore.styles(); + if (styles) { + doc.write(``); + } + doc.write(`
${this._emailContent()}
`); + doc.close(); + + autolink(doc); + + // Notify the EventedIFrame that we've replaced it's document (with `open`) + // so it can attach event listeners again. + this.refs.iframe.documentWasReplaced(); + domNode.height = '0px'; + this._setFrameHeight(); + } + + _getFrameHeight = (doc) => { + if (doc && doc.body) { + return doc.body.scrollHeight; + } + if (doc && doc.documentElement) { + return doc.documentElement.scrollHeight; + } + return 0; + } + + _setFrameHeight = () => { + if (!this._mounted) { + return; + } + + const domNode = React.findDOMNode(this); + const height = this._getFrameHeight(domNode.contentDocument); + + // Why 5px? Some emails have elements with a height of 100%, and then put + // tracking pixels beneath that. In these scenarios, the scrollHeight of the + // message is always <100% + 1px>, which leads us to resize them constantly. + // This is a hack, but I'm not sure of a better solution. + if (Math.abs(height - this._lastComputedHeight) > 5) { + domNode.height = `${height}px`; + this._lastComputedHeight = height; + } + + if (domNode.contentDocument.readyState !== 'complete') { + _.defer(()=> this._setFrameHeight()); + } + } + + render() { + return ( + + ); + } +} diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx index 44f34e669..4286452e8 100644 --- a/internal_packages/message-list/lib/main.cjsx +++ b/internal_packages/message-list/lib/main.cjsx @@ -13,7 +13,6 @@ ThreadArchiveButton = require './thread-archive-button' ThreadTrashButton = require './thread-trash-button' ThreadToggleUnreadButton = require './thread-toggle-unread-button' -AutolinkerExtension = require './plugins/autolinker-extension' TrackingPixelsExtension = require './plugins/tracking-pixels-extension' module.exports = @@ -47,7 +46,6 @@ module.exports = ComponentRegistry.register MessageListHiddenMessagesToggle, role: 'MessageListHeaders' - ExtensionRegistry.MessageView.register AutolinkerExtension ExtensionRegistry.MessageView.register TrackingPixelsExtension deactivate: -> @@ -59,7 +57,6 @@ module.exports = ComponentRegistry.unregister MessageToolbarItems ComponentRegistry.unregister SidebarPluginContainer ComponentRegistry.unregister SidebarParticipantPicker - ExtensionRegistry.MessageView.unregister AutolinkerExtension ExtensionRegistry.MessageView.unregister TrackingPixelsExtension serialize: -> @state diff --git a/internal_packages/message-list/lib/plugins/autolinker-extension.coffee b/internal_packages/message-list/lib/plugins/autolinker-extension.coffee deleted file mode 100644 index 9a88f3f27..000000000 --- a/internal_packages/message-list/lib/plugins/autolinker-extension.coffee +++ /dev/null @@ -1,31 +0,0 @@ -Autolinker = require 'autolinker' -{RegExpUtils, MessageViewExtension} = require 'nylas-exports' - -class AutolinkerExtension extends MessageViewExtension - - @formatMessageBody: ({message}) -> - # Apply the autolinker pass to make emails and links clickable - message.body = Autolinker.link(message.body, {twitter: false}) - - # Ensure that the hrefs in the email always have alt text so you can't hide - # the target of links - # https://regex101.com/r/cH0qM7/1 - titleRe = -> /title\s.*?=\s.*?['"](.*)['"]/gi - message.body = message.body.replace RegExpUtils.linkTagRegex(), (match, openTagPrefix, aTagHref, openTagSuffix, content, closingTag) => - if not content or not closingTag - return match - - openTag = openTagPrefix + aTagHref + openTagSuffix - - if titleRe().test(openTag) - oldTitle = titleRe().exec(openTag)[1] - title = """ title="#{oldTitle} | #{aTagHref}" """ - openTag = openTag.replace(titleRe(), title) - else - title = """ title="#{aTagHref}" """ - tagLen = openTag.length - openTag = openTag.slice(0, tagLen - 1) + title + openTag.slice(tagLen - 1, tagLen) - - return openTag + content + closingTag - -module.exports = AutolinkerExtension diff --git a/internal_packages/message-list/package.json b/internal_packages/message-list/package.json index aafe4e01c..5c4a8d614 100755 --- a/internal_packages/message-list/package.json +++ b/internal_packages/message-list/package.json @@ -7,8 +7,5 @@ "private": true, "engines": { "nylas": "*" - }, - "dependencies": { - "autolinker": "0.18.1" } } diff --git a/internal_packages/message-list/spec/autolinker-extension-spec.coffee b/internal_packages/message-list/spec/autolinker-extension-spec.coffee deleted file mode 100644 index c98b08a07..000000000 --- a/internal_packages/message-list/spec/autolinker-extension-spec.coffee +++ /dev/null @@ -1,28 +0,0 @@ -Autolinker = require 'autolinker' -AutolinkerExtension = require '../lib/plugins/autolinker-extension' - -describe "AutolinkerExtension", -> - beforeEach -> - spyOn(Autolinker, 'link').andCallFake (txt) => txt - - it "should call through to Autolinker", -> - AutolinkerExtension.formatMessageBody(message: {body:'body'}) - expect(Autolinker.link).toHaveBeenCalledWith('body', {twitter: false}) - - it "should add a title to everything with an href", -> - message = - body: """ - hello world! - hello world! - hello world! - hello world! - """ - expected = - body: """ - hello world! - hello world! - hello world! - hello world! - """ - AutolinkerExtension.formatMessageBody({message}) - expect(message.body).toEqual(expected.body) diff --git a/internal_packages/message-list/spec/autolinker-fixtures/gmail-in.html b/internal_packages/message-list/spec/autolinker-fixtures/gmail-in.html new file mode 100644 index 000000000..d2b52345a --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-fixtures/gmail-in.html @@ -0,0 +1,184 @@ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + + + + + + + + + + + +
+New sign-in from Chrome on Mac
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+Hi Ben,
+Your Google Account careless@foundry376.com was just used to sign +in from Chrome on +Mac. + + + + + + + + + + + + + + + + + +
+Ben Gotow (Careless)
+ +careless@foundry376.com
+Mac
+ +Monday, July 13, 2015 3:49 PM (Pacific Daylight Time)
+San Francisco, CA, USA*
+Chrome
+Don't recognize this activity?
+Review your recently used devices now.
+
+Why are we sending this? We take security very seriously and we +want to keep you in the loop on important actions in your +account.
+We were unable to determine whether you have used this browser or +device with your account before. This can happen when you sign in +for the first time on a new computer, phone or browser, when you +use your browser's incognito or private browsing mode or clear your +cookies, or when somebody else is accessing your account.
+Best,
+The Google Accounts team
+*The location is approximate and determined by the IP address it +was coming from.
+This email can't receive replies. To give us feedback on this +alert, click here.
+For more information, visit the Google +Accounts Help Center.
+
+
+You received this mandatory email service announcement to update +you about important changes to your Google product or account.
+
© 2015 Google Inc., +1600 Amphitheatre Parkway, Mountain View, CA 94043, USA
+
+
+ + diff --git a/internal_packages/message-list/spec/autolinker-fixtures/gmail-out.html b/internal_packages/message-list/spec/autolinker-fixtures/gmail-out.html new file mode 100644 index 000000000..e6c5efb6a --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-fixtures/gmail-out.html @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + + + + + + + + + + + +
+New sign-in from Chrome on Mac
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+Hi Ben,
+Your Google Account careless@foundry376.com was just used to sign +in from Chrome on +Mac. + + + + + + + + + + + + + + + + + +
+Ben Gotow (Careless)
+ +careless@foundry376.com
+Mac
+ +Monday, July 13, 2015 3:49 PM (Pacific Daylight Time)
+San Francisco, CA, USA*
+Chrome
+Don't recognize this activity?
+Review your recently used devices now.
+
+Why are we sending this? We take security very seriously and we +want to keep you in the loop on important actions in your +account.
+We were unable to determine whether you have used this browser or +device with your account before. This can happen when you sign in +for the first time on a new computer, phone or browser, when you +use your browser's incognito or private browsing mode or clear your +cookies, or when somebody else is accessing your account.
+Best,
+The Google Accounts team
+*The location is approximate and determined by the IP address it +was coming from.
+This email can't receive replies. To give us feedback on this +alert, click here.
+For more information, visit the Google +Accounts Help Center.
+
+
+You received this mandatory email service announcement to update +you about important changes to your Google product or account.
+
© 2015 Google Inc., +1600 Amphitheatre Parkway, Mountain View, CA 94043, USA
+
+
+ + diff --git a/internal_packages/message-list/spec/autolinker-fixtures/linkedin-in.html b/internal_packages/message-list/spec/autolinker-fixtures/linkedin-in.html new file mode 100644 index 000000000..7cb8be2a7 --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-fixtures/linkedin-in.html @@ -0,0 +1,1248 @@ + + + + + + +" + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
 
+
+
+ + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+
 
+
+ + + + + + + + + +
+ + + + + + +
+
 
+
+
+ + + + + + +
+LinkedIn
+
+ + + + + + +
+
 
+
+ + + + + + +
+
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + + +
+ +Larry Wilmore, Host and Executive Producer at "The Nightly Show +with Larry Wilmore"
+ + + + + + +
+
+
+
+ + + + + + + + + + + + +
+ +Why I Haven’t Worked in 30 Years
+ + + + + + +
+
+
+
+ + + + + + + + + + + + +
+
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ + + + + + +
+
 
+
+
+ + + + + + +
+
+
+
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + + +
+Recommended for you
+ + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+"linkedin.com" + + + + + + +
+
 
+
+
+ +Liz Claman
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +The Letter of Buffett’s Law
+ + + + + + +
+
+
+
+In a +world where people gravitate today to “shorter is better,” a world +where a tweet of 140...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
+"linkedin.com" + + + + + + +
+
 
+
+
+ +Trish Nicolas
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +What To Do When Your Facebook Feed Is Full of Friends Selling +Stuff
+ + + + + + +
+
+
+
+ There +has been blog post after article after video after rant about how +Facebook and other social...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
+"linkedin.com" + + + + + + +
+
 
+
+
+ +Josh Kopelman
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +The Watney Rule for Startups — and the Return to the ‘Old +Normal’
+ + + + + + +
+
+
+
+Founders are +realizing the need to rethink prior assumptions about prioritizing +growth above all...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
+"linkedin.com" + + + + + + +
+
 
+
+
+ +Dr. Travis Bradberry
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +Critical Skills You Should Learn That Pay Dividends +Forever
+ + + + + + +
+
+
+
+The +further along you are in your career, the easier it is to fall back +on the mistaken assumption...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
+"linkedin.com" + + + + + + +
+
 
+
+
+ +Alex Baydin
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +What Every Startup CEO Needs to Know to Not Go Down like Parker +Conrad
+ + + + + + +
+
+
+
+Much +has been made of the recent resignation of Zenefits’ CEO, Parker +Conrad – not because the CEO...
+
+ + + + + + +
+
+
+
+ + + + + + +
+
 
+
+
+
+ + + + + + +
+
+
+
+
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
 
+
+
+Have your own perspective to share?
+ + + + + + +
+
+
+
+Start +writing on LinkedIn
+ + + + + + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
You are receiving notification emails from LinkedIn. +Unsubscribe
This email was intended for Benjamin Hartester (Software +Developer). Learn why we included +this.
If you need assistance or have questions, please contact +LinkedIn Customer +Service.
+ + + + + + +
+
+
+
© 2016 LinkedIn Corporation, 2029 Stierlin Court, Mountain View +CA 94043. LinkedIn and the LinkedIn logo are registered trademarks +of LinkedIn.
+ + + + + + +
+
+
+
+ + + + + + +
+
+
+
+
+ + + + + + +
+
+
+
+
+
+
+ + + + + + +
+
+
+
+" + + diff --git a/internal_packages/message-list/spec/autolinker-fixtures/linkedin-out.html b/internal_packages/message-list/spec/autolinker-fixtures/linkedin-out.html new file mode 100644 index 000000000..12c8c47f0 --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-fixtures/linkedin-out.html @@ -0,0 +1,1010 @@ + + + + + + +" + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
 
+
+
+ + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + + +
+ +Larry Wilmore, Host and Executive Producer at "The Nightly Show +with Larry Wilmore"
+ + + + + + +
+
+
+
+ + + + + + + + + + + + +
+ +Why I Haven’t Worked in 30 Years
+ + + + + + +
+
+
+
+ + + + + + + + + + + + +
+
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + + +
Highlight of the day
+ + + + + + +
+
+
+
+ + + + + + +
+
 
+
+
+ + + + + + +
+
+
+
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + + +
+Recommended for you
+ + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
linkedin.com + + + + + + +
+
 
+
+
+ +Liz Claman
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +The Letter of Buffett’s Law
+ + + + + + +
+
+
+
+In a +world where people gravitate today to “shorter is better,” a world +where a tweet of 140...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
linkedin.com + + + + + + +
+
 
+
+
+ +Trish Nicolas
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +What To Do When Your Facebook Feed Is Full of Friends Selling +Stuff
+ + + + + + +
+
+
+
+ There +has been blog post after article after video after rant about how +Facebook and other social...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
linkedin.com + + + + + + +
+
 
+
+
+ +Josh Kopelman
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +The Watney Rule for Startups — and the Return to the ‘Old +Normal’
+ + + + + + +
+
+
+
+Founders are +realizing the need to rethink prior assumptions about prioritizing +growth above all...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
linkedin.com + + + + + + +
+
 
+
+
+ +Dr. Travis Bradberry
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +Critical Skills You Should Learn That Pay Dividends +Forever
+ + + + + + +
+
+
+
+The +further along you are in your career, the easier it is to fall back +on the mistaken assumption...
+
+ + + + + + +
+
+
+
+ + + + + + + + +
linkedin.com + + + + + + +
+
 
+
+
+ +Alex Baydin
+ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ +What Every Startup CEO Needs to Know to Not Go Down like Parker +Conrad
+ + + + + + +
+
+
+
+Much +has been made of the recent resignation of Zenefits’ CEO, Parker +Conrad – not because the CEO...
+
+ + + + + + +
+
+
+
+ + + + + + +
+
 
+
+
+
+ + + + + + +
+
+
+
+
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
 
+
+
+Have your own perspective to share?
+ + + + + + +
+
+
+
+Start +writing on LinkedIn
+ + + + + + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
You are receiving notification emails from LinkedIn. +Unsubscribe
This email was intended for Benjamin Hartester (Software +Developer). Learn why we included +this.
If you need assistance or have questions, please contact +LinkedIn Customer +Service.
+ + + + + + +
+
+
+
© 2016 LinkedIn Corporation, 2029 Stierlin Court, Mountain View +CA 94043. LinkedIn and the LinkedIn logo are registered trademarks +of LinkedIn.
+ + + + + + +
+
+
+
+ + + + + + +
+
+
+
+
+ + + + + + +
+
+
+
+
+
+
+ + + + + + +
+
+
+
+" + + diff --git a/internal_packages/message-list/spec/autolinker-fixtures/plaintext-in.html b/internal_packages/message-list/spec/autolinker-fixtures/plaintext-in.html new file mode 100644 index 000000000..efc6bd12e --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-fixtures/plaintext-in.html @@ -0,0 +1,26 @@ + + + + + + +http://apple.com/ +

+
https://dropbox.com/
+

+
whatever.com
+

+
kinda-looks-like-a-link.com
+

+
ftp://helloworld.com/asd
+

+
540-250-2334
+

+
+1-524-123-3333
+

+
550.555.1234
+

+
bengotow@gmail.com
+

+ + diff --git a/internal_packages/message-list/spec/autolinker-fixtures/plaintext-out.html b/internal_packages/message-list/spec/autolinker-fixtures/plaintext-out.html new file mode 100644 index 000000000..6c95d320a --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-fixtures/plaintext-out.html @@ -0,0 +1,28 @@ + + + + + + +http://apple.com/ +

+
https://dropbox.com/
+

+
whatever.com
+

+
kinda-looks-like-a-link.com
+

+
ftp://helloworld.com/asd
+

+
540-250-2334
+

+
+1-524-123-3333
+

+
550.555.1234
+

+
bengotow@gmail.com
+

+ + diff --git a/internal_packages/message-list/spec/autolinker-spec.es6 b/internal_packages/message-list/spec/autolinker-spec.es6 new file mode 100644 index 000000000..e99ea155c --- /dev/null +++ b/internal_packages/message-list/spec/autolinker-spec.es6 @@ -0,0 +1,24 @@ +import {autolink} from '../lib/autolinker'; +import fs from 'fs'; +import path from 'path'; + +describe("autolink", () => { + const fixturesDir = path.join(__dirname, 'autolinker-fixtures'); + fs.readdirSync(fixturesDir).filter(filename => + filename.indexOf('-in.html') !== -1 + ).forEach((filename) => { + it(`should properly autolink a variety of email bodies ${filename}`, () => { + const div = document.createElement('div'); + const inputPath = path.join(fixturesDir, filename); + const expectedPath = inputPath.replace('-in', '-out'); + + const input = fs.readFileSync(inputPath).toString(); + const expected = fs.readFileSync(expectedPath).toString(); + + div.innerHTML = input; + autolink({body: div}); + + expect(div.innerHTML).toEqual(expected); + }); + }); +}); diff --git a/src/regexp-utils.coffee b/src/regexp-utils.coffee index feb5ccca8..c1fe58261 100644 --- a/src/regexp-utils.coffee +++ b/src/regexp-utils.coffee @@ -14,6 +14,9 @@ RegExpUtils = # https://en.wikipedia.org/wiki/Email_address#Local_part emailRegex: -> new RegExp(/([a-z.A-Z0-9!#$%&'*+\-/=?^_`{|}~;:]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,63})/g) + # http://stackoverflow.com/questions/16631571/javascript-regular-expression-detect-all-the-phone-number-from-the-page-source + phoneRegex: -> new RegExp(/(?:\+?(\d{1,3}))?[- (]*(\d{3})[- )]*(\d{3})[- ]*(\d{4})(?: *x(\d+))?\b/g) + # http://stackoverflow.com/a/16463966 # http://www.regexpal.com/?fam=93928 # NOTE: This does not match full urls with `http` protocol components. @@ -22,15 +25,12 @@ RegExpUtils = # https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html ipAddressRegex: -> new RegExp(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/i) - # Test cases: https://regex101.com/r/pD7iS5/2 - # http://daringfireball.net/2010/07/improved_regex_for_matching_urls - # https://mathiasbynens.be/demo/url-regex - # This is the Gruber Regex. + # Test cases: https://regex101.com/r/pD7iS5/3 urlRegex: ({matchEntireString} = {}) -> if matchEntireString - new RegExp(/^\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/) + new RegExp(/^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:[\w\-]+\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/g) else - new RegExp(/\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/) + new RegExp(/(?:^|\s)((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:[\w\-]+\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/g) # Test cases: https://regex101.com/r/jD5zC7/2 # Returns the following capturing groups: