dce-reactkit 3.9.0-beta-loading-modal.2 → 3.9.0-beta-loading-modal.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +783 -201
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/client/initClient.d.ts +16 -2
- package/dist/cjs/types/components/AppWrapper.d.ts +32 -0
- package/dist/cjs/types/components/CheckboxButton.d.ts +1 -0
- package/dist/cjs/types/components/Dropdown.d.ts +32 -0
- package/dist/cjs/types/components/RadioButton.d.ts +2 -0
- package/dist/cjs/types/helpers/genRouteHandler.d.ts +8 -0
- package/dist/cjs/types/helpers/validators/ChicagoTitleCase.d.ts +9 -0
- package/dist/cjs/types/helpers/validators/validURL.d.ts +8 -0
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +24 -0
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +33 -0
- package/dist/cjs/types/index.d.ts +5 -2
- package/dist/cjs/types/types/DropdownItemType.d.ts +6 -0
- package/dist/cjs/types/types/ModalType.d.ts +1 -0
- package/dist/cjs/types/types/ReactKitErrorCode.d.ts +6 -1
- package/dist/esm/index.js +779 -202
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/client/initClient.d.ts +16 -2
- package/dist/esm/types/components/AppWrapper.d.ts +32 -0
- package/dist/esm/types/components/CheckboxButton.d.ts +1 -0
- package/dist/esm/types/components/Dropdown.d.ts +32 -0
- package/dist/esm/types/components/RadioButton.d.ts +2 -0
- package/dist/esm/types/helpers/genRouteHandler.d.ts +8 -0
- package/dist/esm/types/helpers/validators/ChicagoTitleCase.d.ts +9 -0
- package/dist/esm/types/helpers/validators/validURL.d.ts +8 -0
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +24 -0
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +33 -0
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/types/DropdownItemType.d.ts +6 -0
- package/dist/esm/types/types/ModalType.d.ts +1 -0
- package/dist/esm/types/types/ReactKitErrorCode.d.ts +6 -1
- package/dist/index.d.ts +163 -46
- package/package.json +2 -1
- package/src/client/initClient.tsx +49 -11
- package/src/components/AppWrapper.tsx +258 -2
- package/src/components/CheckboxButton.tsx +24 -6
- package/src/components/Dropdown.tsx +317 -0
- package/src/components/IntelliTable.tsx +1 -1
- package/src/components/ItemPicker/NestableItemList.tsx +1 -0
- package/src/components/LogReviewer.tsx +17 -4
- package/src/components/Modal/index.tsx +4 -1
- package/src/components/MultiSwitch.tsx +1 -1
- package/src/components/RadioButton.tsx +33 -6
- package/src/components/SimpleDateChooser.tsx +8 -2
- package/src/helpers/genRouteHandler.ts +116 -17
- package/src/helpers/logClientEvent.tsx +8 -0
- package/src/helpers/validators/ChicagoTitleCase.test.ts +85 -0
- package/src/helpers/validators/ChicagoTitleCase.ts +32 -0
- package/src/helpers/validators/findURL.test.ts +83 -0
- package/src/helpers/validators/findURL.ts +43 -0
- package/src/helpers/validators/validURL.test.ts +90 -0
- package/src/helpers/validators/validURL.ts +18 -0
- package/src/helpers/visitEndpointOnAnotherServer/index.ts +93 -0
- package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +164 -0
- package/src/helpers/visitServerEndpoint.tsx +1 -0
- package/src/index.ts +8 -0
- package/src/server/initLogCollection.ts +5 -0
- package/src/server/initServer.ts +11 -5
- package/src/types/DropdownItemType.ts +11 -0
- package/src/types/ModalType.tsx +1 -0
- package/src/types/ReactKitErrorCode.tsx +8 -1
|
@@ -53,6 +53,9 @@ const modalTypeToModalButtonTypes: {
|
|
|
53
53
|
[ModalType.Okay]: [
|
|
54
54
|
ModalButtonType.Okay,
|
|
55
55
|
],
|
|
56
|
+
[ModalType.Cancel]: [
|
|
57
|
+
ModalButtonType.Cancel,
|
|
58
|
+
],
|
|
56
59
|
[ModalType.OkayCancel]: [
|
|
57
60
|
ModalButtonType.Okay,
|
|
58
61
|
ModalButtonType.Cancel,
|
|
@@ -460,7 +463,7 @@ const Modal: React.FC<ModalProps> = (props) => {
|
|
|
460
463
|
// Override sizes for even larger for XL
|
|
461
464
|
width: (
|
|
462
465
|
size === ModalSize.ExtraLarge
|
|
463
|
-
? 'calc(100vw -
|
|
466
|
+
? 'calc(100vw - 1rem)'
|
|
464
467
|
: undefined
|
|
465
468
|
),
|
|
466
469
|
maxWidth: (
|
|
@@ -153,7 +153,7 @@ const MultiSwitch: React.FC<Props> = (props) => {
|
|
|
153
153
|
|
|
154
154
|
// Constants
|
|
155
155
|
const gutterRems = heightRem * 0.1;
|
|
156
|
-
const itemWidthRems = heightRem *
|
|
156
|
+
const itemWidthRems = heightRem * 2.2;
|
|
157
157
|
const textFontSize = heightRem / 3.5;
|
|
158
158
|
const iconFontSize = heightRem / 2;
|
|
159
159
|
const borderWidthRems = 0.05;
|
|
@@ -32,6 +32,8 @@ type Props = {
|
|
|
32
32
|
selected?: boolean,
|
|
33
33
|
// The id of the button
|
|
34
34
|
id?: string,
|
|
35
|
+
// Custom class name of the button
|
|
36
|
+
className?: string,
|
|
35
37
|
// If true, no margin on right
|
|
36
38
|
noMarginOnRight?: boolean,
|
|
37
39
|
// Variant for when radio is selected
|
|
@@ -40,6 +42,8 @@ type Props = {
|
|
|
40
42
|
unselectedVariant?: Variant,
|
|
41
43
|
// True if using a small button
|
|
42
44
|
small?: boolean,
|
|
45
|
+
// If true, use complex formatting (tabs, newlines) in the text
|
|
46
|
+
useComplexFormatting?: boolean,
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
/*------------------------------------------------------------------------*/
|
|
@@ -60,6 +64,7 @@ const RadioButton: React.FC<Props> = (props) => {
|
|
|
60
64
|
title,
|
|
61
65
|
selected,
|
|
62
66
|
id,
|
|
67
|
+
className,
|
|
63
68
|
noMarginOnRight,
|
|
64
69
|
selectedVariant = (
|
|
65
70
|
isDarkModeOn()
|
|
@@ -72,6 +77,7 @@ const RadioButton: React.FC<Props> = (props) => {
|
|
|
72
77
|
: Variant.Light
|
|
73
78
|
),
|
|
74
79
|
small,
|
|
80
|
+
useComplexFormatting,
|
|
75
81
|
} = props;
|
|
76
82
|
|
|
77
83
|
/*------------------------------------------------------------------------*/
|
|
@@ -87,7 +93,7 @@ const RadioButton: React.FC<Props> = (props) => {
|
|
|
87
93
|
type="button"
|
|
88
94
|
id={id}
|
|
89
95
|
title={title}
|
|
90
|
-
className={`btn btn-${selected ? selectedVariant : unselectedVariant}${selected ? ' selected' : ''}${small ? ' btn-sm' : ''} m-0${noMarginOnRight ? '' : ' me-1'}`}
|
|
96
|
+
className={`btn btn-${selected ? selectedVariant : unselectedVariant}${selected ? ' selected' : ''}${small ? ' btn-sm' : ''} m-0${noMarginOnRight ? '' : ' me-1'} ${className ?? ''}`}
|
|
91
97
|
aria-label={`${ariaLabel}${selected ? ': currently selected' : ''}`}
|
|
92
98
|
onClick={() => {
|
|
93
99
|
if (!selected) {
|
|
@@ -95,11 +101,32 @@ const RadioButton: React.FC<Props> = (props) => {
|
|
|
95
101
|
}
|
|
96
102
|
}}
|
|
97
103
|
>
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
<div className="d-flex flex-row align-items-center">
|
|
105
|
+
<div className="me-1">
|
|
106
|
+
<FontAwesomeIcon
|
|
107
|
+
icon={selected ? faDotCircle : faCircle}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
{
|
|
111
|
+
useComplexFormatting
|
|
112
|
+
? (
|
|
113
|
+
<pre
|
|
114
|
+
className="ps-1 text-start text-break m-0"
|
|
115
|
+
style={{
|
|
116
|
+
whiteSpace: 'pre-wrap',
|
|
117
|
+
tabSize: 2,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{text}
|
|
121
|
+
</pre>
|
|
122
|
+
)
|
|
123
|
+
: (
|
|
124
|
+
<div className="flex-grow-1 text-start text-break">
|
|
125
|
+
{text}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
</div>
|
|
103
130
|
</button>
|
|
104
131
|
);
|
|
105
132
|
};
|
|
@@ -94,7 +94,13 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
|
|
|
94
94
|
}
|
|
95
95
|
const monthName = getMonthName(month).full;
|
|
96
96
|
// Year is start year +1 for each 12 months
|
|
97
|
-
|
|
97
|
+
let yearsToAdd = 0;
|
|
98
|
+
let monthsOfYearsToAdd = unmoddedMonth;
|
|
99
|
+
while (monthsOfYearsToAdd > 12) {
|
|
100
|
+
monthsOfYearsToAdd -= 12;
|
|
101
|
+
yearsToAdd += 1;
|
|
102
|
+
}
|
|
103
|
+
const year = startYear + yearsToAdd;
|
|
98
104
|
|
|
99
105
|
// Figure out which days are allowed
|
|
100
106
|
const days = [];
|
|
@@ -174,7 +180,7 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
|
|
|
174
180
|
return (
|
|
175
181
|
<div
|
|
176
182
|
className="SimpleDateChooser d-inline-block"
|
|
177
|
-
aria-label={`date chooser with selected date: ${month}
|
|
183
|
+
aria-label={`date chooser with selected date: ${month}/${day}/${year}`}
|
|
178
184
|
>
|
|
179
185
|
{/* Month Chooser */}
|
|
180
186
|
<select
|
|
@@ -39,6 +39,12 @@ import LogLevel from '../types/LogLevel';
|
|
|
39
39
|
* @param opts.handler function that processes the request
|
|
40
40
|
* @param [opts.skipSessionCheck] if true, skip the session check (allow users
|
|
41
41
|
* to not be logged in and launched via LTI)
|
|
42
|
+
* @param [opts.allowedHosts] if included, only allow requests from these hosts
|
|
43
|
+
* (start a hostname with a "*" to only check the end of the hostname)
|
|
44
|
+
* you can include just one string instead of an array
|
|
45
|
+
* @param [opts.bannedHosts] if included, do not allow requests from these hosts
|
|
46
|
+
* (start a hostname with a "*" to only check the end of the hostname)
|
|
47
|
+
* you can include just one string instead of an array
|
|
42
48
|
* @param [opts.unhandledErrorMessagePrefix] if included, when an error that
|
|
43
49
|
* is not of type ErrorWithCode is thrown, the client will receive an error
|
|
44
50
|
* where the error message is prefixed with this string. For example,
|
|
@@ -102,6 +108,8 @@ const genRouteHandler = (
|
|
|
102
108
|
},
|
|
103
109
|
) => any,
|
|
104
110
|
skipSessionCheck?: boolean,
|
|
111
|
+
allowedHosts?: string[] | string,
|
|
112
|
+
bannedHosts?: string[] | string,
|
|
105
113
|
unhandledErrorMessagePrefix?: string,
|
|
106
114
|
},
|
|
107
115
|
) => {
|
|
@@ -110,6 +118,97 @@ const genRouteHandler = (
|
|
|
110
118
|
// Output params
|
|
111
119
|
const output: { [k in string]: any } = {};
|
|
112
120
|
|
|
121
|
+
/*----------------------------------------*/
|
|
122
|
+
/* ----------- Hostname Check ----------- */
|
|
123
|
+
/*----------------------------------------*/
|
|
124
|
+
|
|
125
|
+
// Get hostnames
|
|
126
|
+
const originURL = String(
|
|
127
|
+
req.get('origin')
|
|
128
|
+
|| req.headers.origin
|
|
129
|
+
|| req.headers.referer,
|
|
130
|
+
);
|
|
131
|
+
const originHostname = (
|
|
132
|
+
originURL
|
|
133
|
+
// Remove protocol
|
|
134
|
+
.replace(/(^\w+:|^)\/\//, '')
|
|
135
|
+
// Remove port
|
|
136
|
+
.replace(/:\d+$/, '')
|
|
137
|
+
);
|
|
138
|
+
const serverHostname = String(req.hostname);
|
|
139
|
+
|
|
140
|
+
// Check allowed
|
|
141
|
+
if (opts.allowedHosts) {
|
|
142
|
+
// Only accept requests from allowed hosts
|
|
143
|
+
const allowedArray = (
|
|
144
|
+
Array.isArray(opts.allowedHosts)
|
|
145
|
+
? opts.allowedHosts
|
|
146
|
+
: [opts.allowedHosts]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Check if server is localhost
|
|
150
|
+
if (serverHostname === 'localhost') {
|
|
151
|
+
// Allow localhost
|
|
152
|
+
allowedArray.push('localhost');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if current host is allowed
|
|
156
|
+
const allowed = allowedArray.some((allowedHost) => {
|
|
157
|
+
if (allowedHost.startsWith('*')) {
|
|
158
|
+
// Check end of hostname
|
|
159
|
+
return originHostname.endsWith(allowedHost.substring(1));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check full hostname
|
|
163
|
+
return originHostname.toLowerCase() === allowedHost.toLowerCase();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// If not allowed, return error
|
|
167
|
+
if (!allowed) {
|
|
168
|
+
return handleError(
|
|
169
|
+
res,
|
|
170
|
+
{
|
|
171
|
+
message: 'You are not allowed to access this endpoint.',
|
|
172
|
+
code: ReactKitErrorCode.HostNotAllowed,
|
|
173
|
+
status: 403,
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check banned
|
|
180
|
+
if (opts.bannedHosts) {
|
|
181
|
+
// Do not allow requests from banned hosts
|
|
182
|
+
const bannedArray = (
|
|
183
|
+
Array.isArray(opts.bannedHosts)
|
|
184
|
+
? opts.bannedHosts
|
|
185
|
+
: [opts.bannedHosts]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Check if current host is banned
|
|
189
|
+
const banned = bannedArray.some((bannedHost) => {
|
|
190
|
+
if (bannedHost.startsWith('*')) {
|
|
191
|
+
// Check end of hostname
|
|
192
|
+
return originHostname.endsWith(bannedHost.substring(1));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check full hostname
|
|
196
|
+
return originHostname.toLowerCase() === bannedHost.toLowerCase();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// If banned, return error
|
|
200
|
+
if (banned) {
|
|
201
|
+
return handleError(
|
|
202
|
+
res,
|
|
203
|
+
{
|
|
204
|
+
message: 'You are not allowed to access this endpoint.',
|
|
205
|
+
code: ReactKitErrorCode.HostBanned,
|
|
206
|
+
status: 403,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
113
212
|
/*----------------------------------------*/
|
|
114
213
|
/* ------------ Parse Params ------------ */
|
|
115
214
|
/*----------------------------------------*/
|
|
@@ -354,22 +453,22 @@ const genRouteHandler = (
|
|
|
354
453
|
output.userId = (
|
|
355
454
|
launchInfo
|
|
356
455
|
? launchInfo.userId
|
|
357
|
-
: undefined
|
|
456
|
+
: (output.userId ?? undefined)
|
|
358
457
|
);
|
|
359
458
|
output.userFirstName = (
|
|
360
459
|
launchInfo
|
|
361
460
|
? launchInfo.userFirstName
|
|
362
|
-
: undefined
|
|
461
|
+
: (output.userFirstName ?? undefined)
|
|
363
462
|
);
|
|
364
463
|
output.userLastName = (
|
|
365
464
|
launchInfo
|
|
366
465
|
? launchInfo.userLastName
|
|
367
|
-
: undefined
|
|
466
|
+
: (output.userLastName ?? undefined)
|
|
368
467
|
);
|
|
369
468
|
output.userEmail = (
|
|
370
469
|
launchInfo
|
|
371
470
|
? launchInfo.userEmail
|
|
372
|
-
: undefined
|
|
471
|
+
: (output.userEmail ?? undefined)
|
|
373
472
|
);
|
|
374
473
|
output.userAvatarURL = (
|
|
375
474
|
launchInfo
|
|
@@ -377,32 +476,32 @@ const genRouteHandler = (
|
|
|
377
476
|
launchInfo.userImage
|
|
378
477
|
?? 'http://www.gravatar.com/avatar/?d=identicon'
|
|
379
478
|
)
|
|
380
|
-
: undefined
|
|
479
|
+
: (output.userAvatarURL ?? undefined)
|
|
381
480
|
);
|
|
382
481
|
output.isLearner = (
|
|
383
482
|
launchInfo
|
|
384
483
|
? !!launchInfo.isLearner
|
|
385
|
-
: undefined
|
|
484
|
+
: (output.isLearner ?? undefined)
|
|
386
485
|
);
|
|
387
486
|
output.isTTM = (
|
|
388
487
|
launchInfo
|
|
389
488
|
? !!launchInfo.isTTM
|
|
390
|
-
: undefined
|
|
489
|
+
: (output.isTTM ?? undefined)
|
|
391
490
|
);
|
|
392
491
|
output.isAdmin = (
|
|
393
492
|
launchInfo
|
|
394
493
|
? !!launchInfo.isAdmin
|
|
395
|
-
: undefined
|
|
494
|
+
: (output.isAdmin ?? undefined)
|
|
396
495
|
);
|
|
397
496
|
output.courseId = (
|
|
398
497
|
launchInfo
|
|
399
498
|
? (output.courseId ?? launchInfo.courseId)
|
|
400
|
-
: undefined
|
|
499
|
+
: (output.courseId ?? undefined)
|
|
401
500
|
);
|
|
402
501
|
output.courseName = (
|
|
403
502
|
launchInfo
|
|
404
503
|
? launchInfo.contextLabel
|
|
405
|
-
: undefined
|
|
504
|
+
: (output.courseName ?? undefined)
|
|
406
505
|
);
|
|
407
506
|
|
|
408
507
|
// Add other session variables
|
|
@@ -526,16 +625,16 @@ const genRouteHandler = (
|
|
|
526
625
|
|
|
527
626
|
// Main log info
|
|
528
627
|
const mainLogInfo: LogMainInfo = {
|
|
529
|
-
id: `${launchInfo ? 'unknown'
|
|
530
|
-
userFirstName: (launchInfo ? 'unknown'
|
|
531
|
-
userLastName: (launchInfo ? 'unknown'
|
|
532
|
-
userEmail: (launchInfo ? 'unknown'
|
|
533
|
-
userId: (launchInfo ? 'unknown'
|
|
628
|
+
id: `${launchInfo ? launchInfo.userId : 'unknown'}-${Date.now()}-${Math.floor(Math.random() * 100000)}-${Math.floor(Math.random() * 100000)}`,
|
|
629
|
+
userFirstName: (launchInfo ? launchInfo.userFirstName: 'unknown'),
|
|
630
|
+
userLastName: (launchInfo ? launchInfo.userLastName : 'unknown'),
|
|
631
|
+
userEmail: (launchInfo ? launchInfo.userEmail : 'unknown'),
|
|
632
|
+
userId: (launchInfo ? launchInfo.userId : 'unknown'),
|
|
534
633
|
isLearner: (launchInfo && !!launchInfo.isLearner),
|
|
535
634
|
isAdmin: (launchInfo && !!launchInfo.isAdmin),
|
|
536
635
|
isTTM: (launchInfo && !!launchInfo.isTTM),
|
|
537
|
-
courseId: (launchInfo ? 'unknown'
|
|
538
|
-
courseName: (launchInfo ? 'unknown'
|
|
636
|
+
courseId: (launchInfo ? launchInfo.courseId : 'unknown' ),
|
|
637
|
+
courseName: (launchInfo ? launchInfo.contextLabel : 'unknown'),
|
|
539
638
|
browser,
|
|
540
639
|
device,
|
|
541
640
|
year,
|
|
@@ -7,6 +7,9 @@ import LogLevel from '../types/LogLevel';
|
|
|
7
7
|
// Import shared functions
|
|
8
8
|
import visitServerEndpoint from './visitServerEndpoint';
|
|
9
9
|
|
|
10
|
+
// Import status
|
|
11
|
+
import { appHasNoServer } from '../client/initClient';
|
|
12
|
+
|
|
10
13
|
/* ------- Metadata Populator ------- */
|
|
11
14
|
|
|
12
15
|
// Type of a metadata populator function
|
|
@@ -36,6 +39,11 @@ export const setClientEventMetadataPopulator = (
|
|
|
36
39
|
* @author Gabe Abrams
|
|
37
40
|
*/
|
|
38
41
|
const logClientEvent: LogFunction = async (opts) => {
|
|
42
|
+
// Don't write to the server if there is none
|
|
43
|
+
if (appHasNoServer()) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
// Populate metadata
|
|
40
48
|
let metadata = (opts.metadata ?? {});
|
|
41
49
|
if (metadataPopulator) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Importing the testing function
|
|
2
|
+
// import chicagoTitleCase from './chicagoTitleCase';
|
|
3
|
+
|
|
4
|
+
// // Import constants
|
|
5
|
+
// import { INVALID_STRING_ERRORS, INVALID_REGEX_ERROR } from './shared/constants/ERROR_MESSAGES';
|
|
6
|
+
|
|
7
|
+
// /*------------------------------------------------------------------------*/
|
|
8
|
+
// /* ---------------------------- Valid Tests --------------------------- */
|
|
9
|
+
// /*------------------------------------------------------------------------*/
|
|
10
|
+
|
|
11
|
+
// const validTests: { input: string, expected: string }[] = [
|
|
12
|
+
// {
|
|
13
|
+
// input: 'this is the american language',
|
|
14
|
+
// expected: 'This the American language',
|
|
15
|
+
// },
|
|
16
|
+
// {
|
|
17
|
+
// input: 'to kill a mockingbird',
|
|
18
|
+
// expected: 'To Kill a Mockingbird',
|
|
19
|
+
// },
|
|
20
|
+
// {
|
|
21
|
+
// input: 'the lord of the rings',
|
|
22
|
+
// expected: 'The Lord of the Rings',
|
|
23
|
+
// },
|
|
24
|
+
// {
|
|
25
|
+
// input: 'pride and prejudice',
|
|
26
|
+
// expected: 'Pride and Prejudice',
|
|
27
|
+
// },
|
|
28
|
+
// {
|
|
29
|
+
// input: 'the road not taken',
|
|
30
|
+
// expected: 'The Road Not Taken',
|
|
31
|
+
// },
|
|
32
|
+
// ];
|
|
33
|
+
|
|
34
|
+
// test(
|
|
35
|
+
// 'Converts strings to Chicago title case correctly.',
|
|
36
|
+
// async () => {
|
|
37
|
+
// validTests.forEach(({ input, expected }) => {
|
|
38
|
+
// expect(chicagoTitleCase(input)).toBe(expected);
|
|
39
|
+
// });
|
|
40
|
+
// },
|
|
41
|
+
// );
|
|
42
|
+
|
|
43
|
+
// /*------------------------------------------------------------------------*/
|
|
44
|
+
// /* ---------------------------- Edge Cases ---------------------------- */
|
|
45
|
+
// /*------------------------------------------------------------------------*/
|
|
46
|
+
|
|
47
|
+
// const edgeCases: { input: string, expected: string }[] = [
|
|
48
|
+
// {
|
|
49
|
+
// input: 'x',
|
|
50
|
+
// expected: 'X',
|
|
51
|
+
// },
|
|
52
|
+
// {
|
|
53
|
+
// input: ' ',
|
|
54
|
+
// expected: ' ',
|
|
55
|
+
// },
|
|
56
|
+
// {
|
|
57
|
+
// input: '',
|
|
58
|
+
// expected: '',
|
|
59
|
+
// },
|
|
60
|
+
// {
|
|
61
|
+
// input: 'the',
|
|
62
|
+
// expected: 'The',
|
|
63
|
+
// },
|
|
64
|
+
// {
|
|
65
|
+
// input: 'THE ROAD NOT TAKEN',
|
|
66
|
+
// expected: 'The Road Not Taken',
|
|
67
|
+
// },
|
|
68
|
+
// {
|
|
69
|
+
// input: 'tO kiLL A mOckingBirD',
|
|
70
|
+
// expected: 'To Kill a Mockingbird',
|
|
71
|
+
// },
|
|
72
|
+
// {
|
|
73
|
+
// input: 'pRiDe And pReduJiCe',
|
|
74
|
+
// expected: 'Pride and Prejudice',
|
|
75
|
+
// },
|
|
76
|
+
// ];
|
|
77
|
+
|
|
78
|
+
// test(
|
|
79
|
+
// 'Handles the edge cases for the function that converts a string to Chicago Title Case.',
|
|
80
|
+
// async () => {
|
|
81
|
+
// edgeCases.forEach(({ input, expected }) => {
|
|
82
|
+
// expect(chicagoTitleCase(input)).toBe(expected);
|
|
83
|
+
// });
|
|
84
|
+
// },
|
|
85
|
+
// );
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function converts a string to title case based on Chicago Title Case
|
|
3
|
+
* Style rules.
|
|
4
|
+
* @author Leisha Bhandari
|
|
5
|
+
* @param input: The input string that needs to be converted to Chicago title
|
|
6
|
+
* case.
|
|
7
|
+
* @returns Input string converted to title case
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const chicagoTitleCase = (input: string): string => {
|
|
12
|
+
// List of lowercase words according to Chicago Manual of Style
|
|
13
|
+
const lowerCaseWords: string[] = [
|
|
14
|
+
'in', 'nor', 'of', 'on', 'or', 'as', 'at', 'but', 'by',
|
|
15
|
+
'for', 'if', 'so', 'the', 'a', 'an', 'and', 'to', 'up',
|
|
16
|
+
'yet'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
return input
|
|
20
|
+
// Converts given input string to lowercase
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
// Splits the given string into separate words
|
|
23
|
+
.split(' ')
|
|
24
|
+
.map((word, index, words) =>
|
|
25
|
+
// Capitalize the first word, last word, and the words that aren't
|
|
26
|
+
// in lowerCaseWords
|
|
27
|
+
index === 0 || !lowerCaseWords.includes(word) || index === words.length - 1
|
|
28
|
+
? `${word.charAt(0).toUpperCase()}${word.slice(1)}` // Capitalize the first letter
|
|
29
|
+
: word // Nothing happens to the word
|
|
30
|
+
)
|
|
31
|
+
.join(' '); // Joins the words back
|
|
32
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Import the function to be tested
|
|
2
|
+
// import findURLs from './findURL';
|
|
3
|
+
|
|
4
|
+
// // Import constants
|
|
5
|
+
// import { INVALID_STRING_ERRORS, INVALID_REGEX_ERROR } from './shared/constants/ERROR_MESSAGES';
|
|
6
|
+
|
|
7
|
+
// /*------------------------------------------------------------------------*/
|
|
8
|
+
// /* ---------------------------- Valid Tests --------------------------- */
|
|
9
|
+
// /*------------------------------------------------------------------------*/
|
|
10
|
+
|
|
11
|
+
// const validTests: { block: string, expected: { start: number, end: number }[] }[] = [
|
|
12
|
+
// {
|
|
13
|
+
// block: 'Visit http://example.com for more information.',
|
|
14
|
+
// expected: [{ start: 6, end: 23 }],
|
|
15
|
+
// },
|
|
16
|
+
// {
|
|
17
|
+
// block: 'Multiple URLs: http://one.com and http://two.com.',
|
|
18
|
+
// expected: [
|
|
19
|
+
// { start: 15, end: 29 },
|
|
20
|
+
// { start: 34, end: 48 },
|
|
21
|
+
// ],
|
|
22
|
+
// },
|
|
23
|
+
// {
|
|
24
|
+
// block: 'Localhost URLs: http://localhost and http://127.0.0.1.',
|
|
25
|
+
// expected: [
|
|
26
|
+
// { start: 16, end: 31 },
|
|
27
|
+
// { start: 36, end: 52 },
|
|
28
|
+
// ],
|
|
29
|
+
// },
|
|
30
|
+
// {
|
|
31
|
+
// block: 'Embedded URL: text https://embedded-url.com?query=1&value=2.',
|
|
32
|
+
// expected: [{ start: 13, end: 51 }],
|
|
33
|
+
// },
|
|
34
|
+
// {
|
|
35
|
+
// block: 'Secure site: https://secure-site.com/path?query=string#fragment',
|
|
36
|
+
// expected: [{ start: 13, end: 58 }],
|
|
37
|
+
// },
|
|
38
|
+
// ];
|
|
39
|
+
|
|
40
|
+
// test(
|
|
41
|
+
// 'Returns correct start and end indices for valid URLs within a string.',
|
|
42
|
+
// async () => {
|
|
43
|
+
// validTests.forEach(({ block, expected }) => {
|
|
44
|
+
// expect(findURLs(block)).toStrictEqual(expected);
|
|
45
|
+
// });
|
|
46
|
+
// },
|
|
47
|
+
// );
|
|
48
|
+
|
|
49
|
+
// /*------------------------------------------------------------------------*/
|
|
50
|
+
// /* ---------------------------- Invalid Tests --------------------------- */
|
|
51
|
+
// /*------------------------------------------------------------------------*/
|
|
52
|
+
|
|
53
|
+
// const invalidTests: { block: string, expected: { start: number, end: number }[] }[] = [
|
|
54
|
+
// {
|
|
55
|
+
// block: 'No URL.',
|
|
56
|
+
// expected: [],
|
|
57
|
+
// },
|
|
58
|
+
// {
|
|
59
|
+
// block: 'URL format is wrong: http:/example.com and htt://example.com.',
|
|
60
|
+
// expected: [],
|
|
61
|
+
// },
|
|
62
|
+
// {
|
|
63
|
+
// block: 'Any punctuation after the URL http://example.com, should not count.',
|
|
64
|
+
// expected: [{ start: 20, end: 37 }],
|
|
65
|
+
// },
|
|
66
|
+
// {
|
|
67
|
+
// block: 'Starting punctuation ,http://example.com should not count.',
|
|
68
|
+
// expected: [{ start: 22, end: 39 }],
|
|
69
|
+
// },
|
|
70
|
+
// {
|
|
71
|
+
// block: 'Spaces in the middle http://exa mple.com is invalid.',
|
|
72
|
+
// expected: [],
|
|
73
|
+
// },
|
|
74
|
+
// ];
|
|
75
|
+
|
|
76
|
+
// test(
|
|
77
|
+
// 'Returns empty array for strings without valid URLs.',
|
|
78
|
+
// async () => {
|
|
79
|
+
// invalidTests.forEach(({ block, expected }) => {
|
|
80
|
+
// expect(findURLs(block)).toStrictEqual(expected);
|
|
81
|
+
// });
|
|
82
|
+
// },
|
|
83
|
+
// );
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// /**
|
|
2
|
+
// * This function finds URLs within a given string and returns an array of
|
|
3
|
+
// * their locations.
|
|
4
|
+
// * @author Leisha Bhandari
|
|
5
|
+
// * @param block: The block of text to search for URLs
|
|
6
|
+
// * @returns Arrays where each of them contain the start and end index of the URL
|
|
7
|
+
// */
|
|
8
|
+
// function findURLs(block: string): { start: number, end: number }[] {
|
|
9
|
+
// // Expression representing the skeleton of a URL to help match and find the
|
|
10
|
+
// // URLs within the block of text (Also takes care of Unicode characters)
|
|
11
|
+
// const urlSkeleton = /(https?:\/\/[^\s\u0000-\u001F.,]+[^\s\u0000-\u001F.,]?(?:\?[^\s\u0000-\u001F]+)?(?:#[^\s\u0000-\u001F]+)?)/g;
|
|
12
|
+
// const found: { start: number, end: number }[] = [];
|
|
13
|
+
// let match: RegExpExecArray | null;
|
|
14
|
+
|
|
15
|
+
// // While loop to find the URLs in the given block of texts using the URL
|
|
16
|
+
// // skeleton
|
|
17
|
+
// while ((match === urlSkeleton.exec(block))
|
|
18
|
+
// && match !== null) {
|
|
19
|
+
// // To find the beginning index of the URL
|
|
20
|
+
// let start = match.index;
|
|
21
|
+
// // To find the last index of the URL
|
|
22
|
+
// let end = match.index + match[0].length;
|
|
23
|
+
|
|
24
|
+
// // If statements to check whether the URL is followed by some kind of
|
|
25
|
+
// // punctuation mark
|
|
26
|
+
// if (block.charAt(end) === '.' || /[,;:!?"]/.test(block.charAt(end))) {
|
|
27
|
+
// end--;
|
|
28
|
+
// }
|
|
29
|
+
|
|
30
|
+
// // While statement to make sure that the URL does not start with a
|
|
31
|
+
// // punctuation mark
|
|
32
|
+
// while (start > 0 && /[.,:;!?]/.test(block.charAt(start - 1))) {
|
|
33
|
+
// start--;
|
|
34
|
+
// }
|
|
35
|
+
|
|
36
|
+
// // The object with start and end index of the URL is found and pushed to
|
|
37
|
+
// // the found array
|
|
38
|
+
// found.push({ start, end });
|
|
39
|
+
// }
|
|
40
|
+
|
|
41
|
+
// // Returns the array of the found URLs location
|
|
42
|
+
// return found;
|
|
43
|
+
// }
|