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
This commit is contained in:
Ben Gotow 2016-03-14 12:30:54 -07:00
parent 1e765a6b62
commit c76582194a
16 changed files with 2853 additions and 160 deletions

View file

@ -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],

View file

@ -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;
}
}

View file

@ -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: =>
<EventedIFrame ref="iframe" seamless="seamless" searchable={true}
onResize={@_setFrameHeight}/>
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 <table> elements use
# the `border-collapse: collapse` css property while setting a
# `padding`.
doc.write("<!DOCTYPE html>")
styles = EmailFrameStylesStore.styles()
if (styles)
doc.write("<style>#{styles}</style>")
doc.write("<div id='inbox-html-wrapper'>#{@_emailContent()}</div>")
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

View file

@ -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 <table> elements use
// the `border-collapse: collapse` css property while setting a
// `padding`.
doc.write("<!DOCTYPE html>");
const styles = EmailFrameStylesStore.styles();
if (styles) {
doc.write(`<style>${styles}</style>`);
}
doc.write(`<div id='inbox-html-wrapper'>${this._emailContent()}</div>`);
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 (
<EventedIFrame
ref="iframe"
seamless="seamless"
searchable
onResize={this._setFrameHeight}
/>
);
}
}

View file

@ -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

View file

@ -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

View file

@ -7,8 +7,5 @@
"private": true,
"engines": {
"nylas": "*"
},
"dependencies": {
"autolinker": "0.18.1"
}
}

View file

@ -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: """
<a href="apple.com">hello world!</a>
<a href = "http://apple.com">hello world!</a>
<a href ='http://apple.com'>hello world!</a>
<a href ='mailto://'>hello world!</a>
"""
expected =
body: """
<a href="apple.com" title="apple.com" >hello world!</a>
<a href = "http://apple.com" title="http://apple.com" >hello world!</a>
<a href ='http://apple.com' title="http://apple.com" >hello world!</a>
<a href ='mailto://' title="mailto://" >hello world!</a>
"""
AutolinkerExtension.formatMessageBody({message})
expect(message.body).toEqual(expected.body)

View file

@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
</head>
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width: 348px;" border=
"0" cellspacing="0" cellpadding="0">
<tr height="32px">
<td></td>
</tr>
<tr align="center">
<td width="32px"></td>
<td>
<table border="0" cellspacing="0" cellpadding="0" style=
"max-width: 600px;">
<tr>
<td>
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="left"><img width="92px" height="32px" src=
"cid:google_logo" style="display: block;"></td>
<td align="right"><img width="32px" height="32px" style=
"display: block;" src="cid:keyhole"></td>
</tr>
</table>
</td>
</tr>
<tr height="16">
<td></td>
</tr>
<tr>
<td>
<table bgcolor="#4184F3" width="100%" border="0" cellspacing="0"
cellpadding="0" style=
"min-width: 332px; max-width: 600px; border: 1px solid #E0E0E0; border-bottom: 0; border-top-left-radius: 3px; border-top-right-radius: 3px;">
<tr>
<td height="72px" colspan="3"></td>
</tr>
<tr>
<td width="32px"></td>
<td style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 24px; color: #FFFFFF; line-height: 1.25;">
New sign-in from Chrome on Mac</td>
<td width="32px"></td>
</tr>
<tr>
<td height="18px" colspan="3"></td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table bgcolor="#FAFAFA" width="100%" border="0" cellspacing="0"
cellpadding="0" style=
"min-width: 332px; max-width: 600px; border: 1px solid #F0F0F0; border-bottom: 1px solid #C0C0C0; border-top: 0; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px;">
<tr height="16px">
<td width="32px" rowspan="3"></td>
<td></td>
<td width="32px" rowspan="3"></td>
</tr>
<tr>
<td>
<table style="min-width: 300px;" border="0" cellspacing="0"
cellpadding="0">
<tr>
<td style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
Hi Ben,</td>
</tr>
<tr>
<td style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
Your Google Account careless@foundry376.com was just used to sign
in from <span style="white-space:nowrap;">Chrome</span> on
<span style="white-space:nowrap;">Mac</span>.
<table border="0" cellspacing="0" cellpadding="0" style=
"margin-top: 48px; margin-bottom: 48px;">
<tr valign="middle">
<td width="32px"></td>
<td align="center"><img src="cid:profilephoto" width="48px" height=
"48px" style="display: block; border-radius: 50%;"></td>
<td width="16px"></td>
<td style="line-height: 1;"><span style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 20px; color: #202020;">
Ben Gotow (Careless)</span><br>
<span style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 13px; color: #727272;">
careless@foundry376.com</span></td>
</tr>
<tr valign="middle">
<td width="32px" height="24px"></td>
<td align="center" height="24px"><img src="cid:down_arrow" width=
"4px" height="10px" style="display: block;"></td>
</tr>
<tr valign="top">
<td width="32px"></td>
<td align="center"><img src="cid:osx" width="48px" height="48px"
style="display: block;"></td>
<td width="16px"></td>
<td style="line-height: 1.5;"><span style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 16px; color: #202020;">
Mac</span><br>
<span style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #727272;">
Monday, July 13, 2015 3:49 PM (Pacific Daylight Time)<br>
San Francisco, CA, USA*<br>
Chrome</span></td>
</tr>
</table>
<b>Don't recognize this activity?</b><br>
Review your <a href=
"https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&amp;am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31"
style="text-decoration: none; color: #4285F4;" target=
"_blank">recently used devices</a> now.<br>
<br>
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.<br>
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.</td>
</tr>
<tr height="32px">
<td></td>
</tr>
<tr>
<td style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
Best,<br>
The Google Accounts team</td>
</tr>
<tr height="16px">
<td></td>
</tr>
<tr>
<td style=
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: #B9B9B9; line-height: 1.5;">
*The location is approximate and determined by the IP address it
was coming from.<br>
This email can't receive replies. To give us feedback on this
alert, <a href=
"https://support.google.com/accounts/contact/device_alert_feedback?hl=en"
style="text-decoration: none; color: #4285F4;" target=
"_blank">click here</a>.<br>
For more information, visit the <a href=
"https://support.google.com/accounts/answer/2733203" style=
"text-decoration: none; color: #4285F4;" target="_blank">Google
Accounts Help Center</a>.</td>
</tr>
</table>
</td>
</tr>
<tr height="32px">
<td></td>
</tr>
</table>
</td>
</tr>
<tr height="16">
<td></td>
</tr>
<tr>
<td style=
"max-width: 600px; font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 10px; color: #BCBCBC; line-height: 1.5;">
You received this mandatory email service announcement to update
you about important changes to your Google product or account.<br>
<div style="direction: ltr; text-align: left">© 2015 Google Inc.,
1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</div>
</td>
</tr>
</table>
</td>
<td width="32px"></td>
</tr>
<tr height="32px">
<td></td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,153 @@
<title></title>
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0">
<tbody><tr height="32px">
<td></td>
</tr>
<tr align="center">
<td width="32px"></td>
<td>
<table border="0" cellspacing="0" cellpadding="0" style="max-width: 600px;">
<tbody><tr>
<td>
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tbody><tr>
<td align="left"><img width="92px" height="32px" src="cid:google_logo" style="display: block;"></td>
<td align="right"><img width="32px" height="32px" style="display: block;" src="cid:keyhole"></td>
</tr>
</tbody></table>
</td>
</tr>
<tr height="16">
<td></td>
</tr>
<tr>
<td>
<table bgcolor="#4184F3" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width: 332px; max-width: 600px; border: 1px solid #E0E0E0; border-bottom: 0; border-top-left-radius: 3px; border-top-right-radius: 3px;">
<tbody><tr>
<td height="72px" colspan="3"></td>
</tr>
<tr>
<td width="32px"></td>
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 24px; color: #FFFFFF; line-height: 1.25;">
New sign-in from Chrome on Mac</td>
<td width="32px"></td>
</tr>
<tr>
<td height="18px" colspan="3"></td>
</tr>
</tbody></table>
</td>
</tr>
<tr>
<td>
<table bgcolor="#FAFAFA" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width: 332px; max-width: 600px; border: 1px solid #F0F0F0; border-bottom: 1px solid #C0C0C0; border-top: 0; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px;">
<tbody><tr height="16px">
<td width="32px" rowspan="3"></td>
<td></td>
<td width="32px" rowspan="3"></td>
</tr>
<tr>
<td>
<table style="min-width: 300px;" border="0" cellspacing="0" cellpadding="0">
<tbody><tr>
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
Hi Ben,</td>
</tr>
<tr>
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
Your Google Account <a href="mailto:careless@foundry376.com" title="mailto:careless@foundry376.com">careless@foundry376.com</a> was just used to sign
in from <span style="white-space:nowrap;">Chrome</span> on
<span style="white-space:nowrap;">Mac</span>.
<table border="0" cellspacing="0" cellpadding="0" style="margin-top: 48px; margin-bottom: 48px;">
<tbody><tr valign="middle">
<td width="32px"></td>
<td align="center"><img src="cid:profilephoto" width="48px" height="48px" style="display: block; border-radius: 50%;"></td>
<td width="16px"></td>
<td style="line-height: 1;"><span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 20px; color: #202020;">
Ben Gotow (Careless)</span><br>
<span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 13px; color: #727272;">
<a href="mailto:careless@foundry376.com" title="mailto:careless@foundry376.com">careless@foundry376.com</a></span></td>
</tr>
<tr valign="middle">
<td width="32px" height="24px"></td>
<td align="center" height="24px"><img src="cid:down_arrow" width="4px" height="10px" style="display: block;"></td>
</tr>
<tr valign="top">
<td width="32px"></td>
<td align="center"><img src="cid:osx" width="48px" height="48px" style="display: block;"></td>
<td width="16px"></td>
<td style="line-height: 1.5;"><span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 16px; color: #202020;">
Mac</span><br>
<span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #727272;">
Monday, July 13, 2015 3:49 PM (Pacific Daylight Time)<br>
San Francisco, CA, USA*<br>
Chrome</span></td>
</tr>
</tbody></table>
<b>Don't recognize this activity?</b><br>
Review your <a href="https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&amp;am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31" style="text-decoration: none; color: #4285F4;" target="_blank" title="https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&amp;am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31">recently used devices</a> now.<br>
<br>
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.<br>
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.</td>
</tr>
<tr height="32px">
<td></td>
</tr>
<tr>
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
Best,<br>
The Google Accounts team</td>
</tr>
<tr height="16px">
<td></td>
</tr>
<tr>
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: #B9B9B9; line-height: 1.5;">
*The location is approximate and determined by the IP address it
was coming from.<br>
This email can't receive replies. To give us feedback on this
alert, <a href="https://support.google.com/accounts/contact/device_alert_feedback?hl=en" style="text-decoration: none; color: #4285F4;" target="_blank" title="https://support.google.com/accounts/contact/device_alert_feedback?hl=en">click here</a>.<br>
For more information, visit the <a href="https://support.google.com/accounts/answer/2733203" style="text-decoration: none; color: #4285F4;" target="_blank" title="https://support.google.com/accounts/answer/2733203">Google
Accounts Help Center</a>.</td>
</tr>
</tbody></table>
</td>
</tr>
<tr height="32px">
<td></td>
</tr>
</tbody></table>
</td>
</tr>
<tr height="16">
<td></td>
</tr>
<tr>
<td style="max-width: 600px; font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 10px; color: #BCBCBC; line-height: 1.5;">
You received this mandatory email service announcement to update
you about important changes to your Google product or account.<br>
<div style="direction: ltr; text-align: left">© 2015 Google Inc.,
1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</div>
</td>
</tr>
</tbody></table>
</td>
<td width="32px"></td>
</tr>
<tr height="32px">
<td></td>
</tr>
</tbody></table>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
http://apple.com/
<div><br></div>
<div>https://dropbox.com/</div>
<div><br></div>
<div>whatever.com</div>
<div><br></div>
<div>kinda-looks-like-a-link.com</div>
<div><br></div>
<div>ftp://helloworld.com/asd</div>
<div><br></div>
<div>540-250-2334</div>
<div><br></div>
<div>+1-524-123-3333</div>
<div><br></div>
<div>550.555.1234</div>
<div><br></div>
<div>bengotow@gmail.com</div>
<div><br></div>
</body>
</html>

View file

@ -0,0 +1,28 @@
<title></title>
<a href="
http://apple.com/" title="
http://apple.com/">
http://apple.com/</a>
<div><br></div>
<div><a href="https://dropbox.com/" title="https://dropbox.com/">https://dropbox.com/</a></div>
<div><br></div>
<div><a href="whatever.com" title="whatever.com">whatever.com</a></div>
<div><br></div>
<div><a href="kinda-looks-like-a-link.com" title="kinda-looks-like-a-link.com">kinda-looks-like-a-link.com</a></div>
<div><br></div>
<div><a href="ftp://helloworld.com/asd" title="ftp://helloworld.com/asd">ftp://helloworld.com/asd</a></div>
<div><br></div>
<div><a href="tel:540-250-2334" title="tel:540-250-2334">540-250-2334</a></div>
<div><br></div>
<div><a href="tel:+1-524-123-3333" title="tel:+1-524-123-3333">+1-524-123-3333</a></div>
<div><br></div>
<div><a href="550.555.1234" title="550.555.1234">550.555.1234</a></div>
<div><br></div>
<div><a href="mailto:bengotow@gmail.com" title="mailto:bengotow@gmail.com">bengotow@gmail.com</a></div>
<div><br></div>

View file

@ -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);
});
});
});

View file

@ -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: