Skip to content
This repository was archived by the owner on Feb 26, 2025. It is now read-only.

Commit a58d86a

Browse files
jasonleibowitzBrian Wyant
and
Brian Wyant
authored
Escape New Lines in Generated ICS (#37)
Create new util escapeICSDescription - Used to build description in buildShareFile - Create unitTest - Update docs with example Co-authored-by: Brian Wyant <bw@goldenvolunteer.com>
1 parent 6206080 commit a58d86a

File tree

7 files changed

+111
-40
lines changed

7 files changed

+111
-40
lines changed

docs/bundle.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
testURL: 'http://localhost/'
3+
};

lib/utils.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
6-
exports.buildShareUrl = exports.isInternetExplorer = exports.isMobile = exports.formatDuration = exports.formatDate = void 0;
6+
exports.buildShareUrl = exports.escapeICSDescription = exports.isInternetExplorer = exports.isMobile = exports.formatDuration = exports.formatDate = void 0;
77

88
var _enums = require("./enums");
99

@@ -53,6 +53,12 @@ exports.isMobile = isMobile;
5353
var isInternetExplorer = function isInternetExplorer() {
5454
return /MSIE/.test(window.navigator.userAgent) || /Trident/.test(window.navigator.userAgent);
5555
};
56+
57+
exports.isInternetExplorer = isInternetExplorer;
58+
59+
var escapeICSDescription = function escapeICSDescription(description) {
60+
return description.replace(/(\r?\n|<br ?\/?>)/g, '\\n');
61+
};
5662
/**
5763
* Takes an event object and returns a Google Calendar Event URL
5864
* @param {string} event.description
@@ -64,7 +70,7 @@ var isInternetExplorer = function isInternetExplorer() {
6470
*/
6571

6672

67-
exports.isInternetExplorer = isInternetExplorer;
73+
exports.escapeICSDescription = escapeICSDescription;
6874

6975
var googleShareUrl = function googleShareUrl(_ref) {
7076
var description = _ref.description,
@@ -120,7 +126,7 @@ var buildShareFile = function buildShareFile(_ref3) {
120126
title = _ref3$title === void 0 ? '' : _ref3$title;
121127
var content = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'BEGIN:VEVENT', "URL:".concat(document.URL), 'METHOD:PUBLISH', // TODO: Will need to parse the date without Z for ics
122128
// This means I'll probably have to require a date lib - luxon most likely or datefns
123-
timezone === '' ? "DTSTART:".concat(startDatetime) : "DTSTART;TZID=".concat(timezone, ":").concat(startDatetime), timezone === '' ? "DTEND:".concat(endDatetime) : "DTEND;TZID=".concat(timezone, ":").concat(endDatetime), "SUMMARY:".concat(title), "DESCRIPTION:".concat(description), "LOCATION:".concat(location), 'END:VEVENT', 'END:VCALENDAR'].join('\n');
129+
timezone === '' ? "DTSTART:".concat(startDatetime) : "DTSTART;TZID=".concat(timezone, ":").concat(startDatetime), timezone === '' ? "DTEND:".concat(endDatetime) : "DTEND;TZID=".concat(timezone, ":").concat(endDatetime), "SUMMARY:".concat(title), "DESCRIPTION:".concat(escapeICSDescription(description)), "LOCATION:".concat(location), 'END:VEVENT', 'END:VCALENDAR'].join('\n');
124130
return isMobile() ? encodeURI("data:text/calendar;charset=utf8,".concat(content)) : content;
125131
};
126132
/**

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/docs/index.jsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,31 @@ function Demo() {
147147
`}
148148
</CodeSnippet>
149149

150+
<h2 className={subTitleStyles}>Dropdown Example - Handle Newlines in Description</h2>
151+
<AddToCalendarDropdown
152+
className={componentStyles}
153+
linkProps={{
154+
className: linkStyles,
155+
}}
156+
event={{
157+
...event,
158+
description: 'Description of event. <br>Going to have a lot of fun doing things that we scheduled ahead of time.'
159+
}}
160+
/>
161+
<CodeSnippet>
162+
{`
163+
const AddToCalendarDropdown = AddToCalendarHOC(Button, Dropdown);
164+
...
165+
<AddToCalendarDropdown
166+
className={componentStyles}
167+
linkProps={{
168+
className: linkStyles,
169+
}}
170+
event={event}
171+
/>
172+
`}
173+
</CodeSnippet>
174+
150175
<h2 className={subTitleStyles}>Modal Example</h2>
151176
<AddToCalendarModal
152177
className={componentStyles}
@@ -299,7 +324,7 @@ function Demo() {
299324
`}
300325
</CodeSnippet>
301326

302-
<h2 className={subTitleStyles}>Use Moment Alternative</h2>
327+
<h2 className={subTitleStyles}>Use Moment Alternative</h2>
303328
<p className={paragraphStyles}>Moment is known to be a MASSIVE library. v2.22.2 is 64.2kb minified + gzipped and moment-timezone v0.5.21 is 89.8kb minified + gzipped. There are plenty of other date time libraries for JS that are way smaller. Using one of these helps you avoid overly bloating your application and sending too many vendor files to the client. One great option is Luxon. Luxon v.1.4.4 is 16.9kb minified + gzipped.</p>
304329
<p className={paragraphStyles}>This example shows how to use the Luxon library (instead of Moment) to construct <span className={highlightText}>startDatetime</span> and <span className={highlightText}>endDatetime</span></p>
305330
<AddToCalendarModal

src/lib/utils.js

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { SHARE_SITES } from './enums';
1+
import {
2+
SHARE_SITES
3+
} from './enums';
24

35

46
/**
@@ -31,6 +33,8 @@ export const isMobile = () => /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile
3133
*/
3234
export const isInternetExplorer = () => /MSIE/.test(window.navigator.userAgent) || /Trident/.test(window.navigator.userAgent);
3335

36+
export const escapeICSDescription = description => description.replace(/(\r?\n|<br ?\/?>)/g, '\\n');
37+
3438
/**
3539
* Takes an event object and returns a Google Calendar Event URL
3640
* @param {string} event.description
@@ -41,13 +45,13 @@ export const isInternetExplorer = () => /MSIE/.test(window.navigator.userAgent)
4145
* @returns {string} Google Calendar Event URL
4246
*/
4347
const googleShareUrl = ({
44-
description,
45-
endDatetime,
46-
location,
47-
startDatetime,
48-
timezone,
49-
title,
50-
}) =>
48+
description,
49+
endDatetime,
50+
location,
51+
startDatetime,
52+
timezone,
53+
title,
54+
}) =>
5155
`https://calendar.google.com/calendar/render?action=TEMPLATE&dates=${
5256
startDatetime
5357
}/${endDatetime}${timezone && `&ctz=${timezone}`}&location=${location}&text=${title}&details=${description}`;
@@ -62,12 +66,12 @@ const googleShareUrl = ({
6266
* @returns {string} Yahoo Calendar Event URL
6367
*/
6468
const yahooShareUrl = ({
65-
description,
66-
duration,
67-
location,
68-
startDatetime,
69-
title,
70-
}) =>
69+
description,
70+
duration,
71+
location,
72+
startDatetime,
73+
title,
74+
}) =>
7175
`https://calendar.yahoo.com/?v=60&view=d&type=20&title=${title}&st=${
7276
startDatetime
7377
}&dur=${duration}&desc=${description}&in_loc=${location}`;
@@ -101,7 +105,7 @@ const buildShareFile = ({
101105
timezone === '' ? `DTSTART:${startDatetime}` : `DTSTART;TZID=${timezone}:${startDatetime}`,
102106
timezone === '' ? `DTEND:${endDatetime}` : `DTEND;TZID=${timezone}:${endDatetime}`,
103107
`SUMMARY:${title}`,
104-
`DESCRIPTION:${description}`,
108+
`DESCRIPTION:${escapeICSDescription(description)}`,
105109
`LOCATION:${location}`,
106110
'END:VEVENT',
107111
'END:VCALENDAR',
@@ -121,8 +125,15 @@ const buildShareFile = ({
121125
* @param {string} event.title
122126
* @param {enum} type One of SHARE_SITES from ./enums
123127
*/
124-
export const buildShareUrl = (
125-
{ description = '', duration, endDatetime, location = '', startDatetime, timezone = '', title = '' },
128+
export const buildShareUrl = ({
129+
description = '',
130+
duration,
131+
endDatetime,
132+
location = '',
133+
startDatetime,
134+
timezone = '',
135+
title = ''
136+
},
126137
type,
127138
) => {
128139
const encodeURI = type !== SHARE_SITES.ICAL && type !== SHARE_SITES.OUTLOOK;
@@ -145,4 +156,4 @@ export const buildShareUrl = (
145156
default:
146157
return buildShareFile(data);
147158
}
148-
};
159+
};

src/lib/utils.test.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import moment from 'moment';
2-
import { SHARE_SITES } from './enums';
3-
import { buildShareUrl, formatDate, formatDuration, isInternetExplorer, isMobile } from './utils';
2+
import {
3+
SHARE_SITES
4+
} from './enums';
5+
import {
6+
buildShareUrl,
7+
formatDate,
8+
formatDuration,
9+
isInternetExplorer,
10+
isMobile,
11+
escapeICSDescription
12+
} from './utils';
413

514
const testEvent = {
615
description: 'Description of event. Going to have a lot of fun doing things that we scheduled ahead of time.',
@@ -14,8 +23,8 @@ const testEvent = {
1423
const expectedOutputs = {
1524
google: 'https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20150126T000000Z/20150126T020000Z&location=NYC&text=Super%20Fun%20Event&details=Description%20of%20event.%20Going%20to%20have%20a%20lot%20of%20fun%20doing%20things%20that%20we%20scheduled%20ahead%20of%20time.',
1625
yahoo: 'https://calendar.yahoo.com/?v=60&view=d&type=20&title=Super%20Fun%20Event&st=20150126T000000Z&dur=0200&desc=Description%20of%20event.%20Going%20to%20have%20a%20lot%20of%20fun%20doing%20things%20that%20we%20scheduled%20ahead%20of%20time.&in_loc=NYC',
17-
ics: 'BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nURL:about:blank\nMETHOD:PUBLISH\nDTSTART:20150126T000000Z\nDTEND:20150126T020000Z\nSUMMARY:Super Fun Event\nDESCRIPTION:Description of event. Going to have a lot of fun doing things that we scheduled ahead of time.\nLOCATION:NYC\nEND:VEVENT\nEND:VCALENDAR',
18-
icsMobile: 'data:text/calendar;charset=utf8,BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nURL:about:blank\nMETHOD:PUBLISH\nDTSTART:20150126T000000Z\nDTEND:20150126T020000Z\nSUMMARY:Super Fun Event\nDESCRIPTION:Description of event. Going to have a lot of fun doing things that we scheduled ahead of time.\nLOCATION:NYC\nEND:VEVENT\nEND:VCALENDAR',
26+
ics: 'BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nURL:http://localhost/\nMETHOD:PUBLISH\nDTSTART:20150126T000000Z\nDTEND:20150126T020000Z\nSUMMARY:Super Fun Event\nDESCRIPTION:Description of event. Going to have a lot of fun doing things that we scheduled ahead of time.\nLOCATION:NYC\nEND:VEVENT\nEND:VCALENDAR',
27+
icsMobile: 'data:text/calendar;charset=utf8,BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nURL:http://localhost/\nMETHOD:PUBLISH\nDTSTART:20150126T000000Z\nDTEND:20150126T020000Z\nSUMMARY:Super Fun Event\nDESCRIPTION:Description of event. Going to have a lot of fun doing things that we scheduled ahead of time.\nLOCATION:NYC\nEND:VEVENT\nEND:VCALENDAR',
1928
}
2029

2130
describe('formatDate', () => {
@@ -25,7 +34,7 @@ describe('formatDate', () => {
2534
});
2635

2736
describe('formatDuration', () => {
28-
it ('converts number 2 to string 0200', () => {
37+
it('converts number 2 to string 0200', () => {
2938
expect(formatDuration(2)).toEqual('0200');
3039
});
3140

@@ -53,7 +62,10 @@ describe('buildShareUrl', () => {
5362
});
5463

5564
it('returns a proper Yahoo share URL when duration is a number', () => {
56-
const result = buildShareUrl({...testEvent, duration: 2}, SHARE_SITES.YAHOO);
65+
const result = buildShareUrl({
66+
...testEvent,
67+
duration: 2
68+
}, SHARE_SITES.YAHOO);
5769
expect(result).toEqual(expectedOutputs.yahoo);
5870
});
5971
});
@@ -66,7 +78,7 @@ describe('buildShareUrl', () => {
6678
});
6779

6880
it('prepends a data URL when userAgent is mobile', () => {
69-
navigator.__defineGetter__('userAgent', function(){
81+
navigator.__defineGetter__('userAgent', function () {
7082
return "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1";
7183
});
7284

@@ -78,7 +90,7 @@ describe('buildShareUrl', () => {
7890

7991
describe('isInternetExplorer', () => {
8092
it('returns true is userAgent is IE 11', () => {
81-
navigator.__defineGetter__('userAgent', function(){
93+
navigator.__defineGetter__('userAgent', function () {
8294
return "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; rv:11.0) like Gecko";
8395
});
8496

@@ -87,7 +99,7 @@ describe('isInternetExplorer', () => {
8799
});
88100

89101
it('returns true is userAgent is IE 10', () => {
90-
navigator.__defineGetter__('userAgent', function(){
102+
navigator.__defineGetter__('userAgent', function () {
91103
return "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729)";
92104
});
93105

@@ -96,7 +108,7 @@ describe('isInternetExplorer', () => {
96108
});
97109

98110
it('returns true is userAgent is IE 9', () => {
99-
navigator.__defineGetter__('userAgent', function(){
111+
navigator.__defineGetter__('userAgent', function () {
100112
return "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)";
101113
});
102114

@@ -105,7 +117,7 @@ describe('isInternetExplorer', () => {
105117
});
106118

107119
it('returns false is userAgent is MS Edge', () => {
108-
navigator.__defineGetter__('userAgent', function(){
120+
navigator.__defineGetter__('userAgent', function () {
109121
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36";
110122
});
111123

@@ -114,7 +126,7 @@ describe('isInternetExplorer', () => {
114126
});
115127

116128
it('returns false is userAgent is not IE', () => {
117-
navigator.__defineGetter__('userAgent', function(){
129+
navigator.__defineGetter__('userAgent', function () {
118130
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36";
119131
});
120132

@@ -125,7 +137,7 @@ describe('isInternetExplorer', () => {
125137

126138
describe('isMobile', () => {
127139
it('returns true if userAgent is iPhone', () => {
128-
navigator.__defineGetter__('userAgent', function(){
140+
navigator.__defineGetter__('userAgent', function () {
129141
return "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1";
130142
});
131143

@@ -134,7 +146,7 @@ describe('isMobile', () => {
134146
});
135147

136148
it('returns true if userAgent is Android', () => {
137-
navigator.__defineGetter__('userAgent', function(){
149+
navigator.__defineGetter__('userAgent', function () {
138150
return "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Mobile Safari/537.36";
139151
});
140152

@@ -143,11 +155,25 @@ describe('isMobile', () => {
143155
});
144156

145157
it('returns false if userAgent is desktop', () => {
146-
navigator.__defineGetter__('userAgent', function(){
158+
navigator.__defineGetter__('userAgent', function () {
147159
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36";
148160
});
149161

150162
const result = isMobile();
151163
expect(result).toBe(false);
152164
})
165+
});
166+
167+
describe('escapeICSDescription', () => {
168+
it('replaces carriage returns with newline characters', () => {
169+
const result = escapeICSDescription('Line One \r\nLine Two');
170+
expect(result).toEqual('Line One \\nLine Two');
171+
});
172+
173+
it('replaces <br> characters with newline characters', () => {
174+
const expectedResult = 'Line One \\nLineTwo';
175+
expect(escapeICSDescription('Line One <br>LineTwo')).toEqual(expectedResult);
176+
expect(escapeICSDescription('Line One <br />LineTwo')).toEqual(expectedResult);
177+
expect(escapeICSDescription('Line One <br >LineTwo')).toEqual(expectedResult);
178+
});
153179
});

0 commit comments

Comments
 (0)