Skip to content

Commit 94eb3cd

Browse files
committed
Improve code coverage, working on truncate-smart.ts
1 parent 53de679 commit 94eb3cd

File tree

9 files changed

+150
-106
lines changed

9 files changed

+150
-106
lines changed

.nycrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"text",
55
"text-summary",
66
"html",
7-
"json-summary",
87
"lcovonly"
98
],
109
"report-dir": "coverage",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[![build](https://github.com/gregjacobs/Autolinker.js/actions/workflows/main.yml/badge.svg)](https://github.com/gregjacobs/Autolinker.js/actions/workflows/main.yml)
55
[![NPM Downloads](https://img.shields.io/npm/dw/autolinker)](https://www.npmjs.com/package/autolinker)
66
[![GitHub License](https://img.shields.io/github/license/gregjacobs/Autolinker.js)](https://github.com/gregjacobs/Autolinker.js/blob/master/LICENSE)
7+
<!--[![codecov](https://codecov.io/github/gregjacobs/Autolinker.js/graph/badge.svg?token=6sqLqa2oeb)](https://codecov.io/github/gregjacobs/Autolinker.js)-->
78

89
Because I had so much trouble finding a good auto-linking implementation out in
910
the wild, I decided to roll my own. It seemed that everything I found out there

src/htmlParser/parse-html.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export function parseHtml(
159159
stateDoctype(char);
160160
break;
161161

162+
/* istanbul ignore next */
162163
default:
163164
assertNever(state);
164165
}

src/match/hashtag-match.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ export class HashtagMatch extends AbstractMatch {
100100
case 'youtube':
101101
return 'https://youtube.com/hashtag/' + hashtag;
102102

103+
/* istanbul ignore next */
103104
default:
104-
// Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case
105+
// Should never happen because Autolinker's constructor should block any invalid values, but just in case
105106
assertNever(serviceName);
106-
throw new Error(`Invalid hashtag service: ${serviceName}`);
107107
}
108108
}
109109

src/match/mention-match.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ export class MentionMatch extends AbstractMatch {
9595
case 'youtube':
9696
return 'https://youtube.com/@' + this.mention;
9797

98+
/* istanbul ignore next */
9899
default:
99-
// Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case.
100+
// Should never happen because Autolinker's constructor should block any invalid values, but just in case.
100101
assertNever(this.serviceName);
101-
throw new Error('Unknown service name to point mention to: ' + this.serviceName);
102102
}
103103
}
104104

src/parser/parse-matches.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export function parseMatches(text: string, args: ParseMatchesArgs): Match[] {
215215
statePhoneNumberPoundChar(stateMachine, char);
216216
break;
217217

218+
/* istanbul ignore next */
218219
default:
219220
assertNever(stateMachine.state);
220221
}
@@ -920,6 +921,7 @@ export function parseMatches(text: string, args: ParseMatchesArgs): Match[] {
920921
return; // not a valid match
921922
}
922923
} else {
924+
/* istanbul ignore next */
923925
assertNever(urlMatchType);
924926
}
925927

@@ -994,6 +996,7 @@ export function parseMatches(text: string, args: ParseMatchesArgs): Match[] {
994996
);
995997
}
996998
} else {
999+
/* istanbul ignore next */
9971000
assertNever(stateMachine);
9981001
}
9991002
}

src/truncate/truncate-smart.ts

Lines changed: 128 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -23,108 +23,56 @@ export function truncateSmart(url: string, truncateLen: number, ellipsisChars?:
2323
ellipsisLengthBeforeParsing = ellipsisChars.length;
2424
}
2525

26-
let parse_url = function (url: string) {
27-
// Functionality inspired by PHP function of same name
28-
let urlObj: UrlObject = {};
29-
let urlSub = url;
30-
let match = urlSub.match(/^([a-z]+):\/\//i);
31-
if (match) {
32-
urlObj.scheme = match[1];
33-
urlSub = urlSub.substr(match[0].length);
34-
}
35-
match = urlSub.match(/^(.*?)(?=(\?|#|\/|$))/i);
36-
if (match) {
37-
urlObj.host = match[1];
38-
urlSub = urlSub.substr(match[0].length);
39-
}
40-
match = urlSub.match(/^\/(.*?)(?=(\?|#|$))/i);
41-
if (match) {
42-
urlObj.path = match[1];
43-
urlSub = urlSub.substr(match[0].length);
44-
}
45-
match = urlSub.match(/^\?(.*?)(?=(#|$))/i);
46-
if (match) {
47-
urlObj.query = match[1];
48-
urlSub = urlSub.substr(match[0].length);
49-
}
50-
match = urlSub.match(/^#(.*?)$/i);
51-
if (match) {
52-
urlObj.fragment = match[1];
53-
//urlSub = urlSub.substr(match[0].length); -- not used. Uncomment if adding another block.
54-
}
55-
return urlObj;
56-
};
57-
58-
let buildUrl = function (urlObj: UrlObject) {
59-
let url = '';
60-
if (urlObj.scheme && urlObj.host) {
61-
url += urlObj.scheme + '://';
62-
}
63-
if (urlObj.host) {
64-
url += urlObj.host;
65-
}
66-
if (urlObj.path) {
67-
url += '/' + urlObj.path;
68-
}
69-
if (urlObj.query) {
70-
url += '?' + urlObj.query;
71-
}
72-
if (urlObj.fragment) {
73-
url += '#' + urlObj.fragment;
74-
}
75-
return url;
76-
};
77-
78-
let buildSegment = function (segment: string, remainingAvailableLength: number) {
79-
let remainingAvailableLengthHalf = remainingAvailableLength / 2,
80-
startOffset = Math.ceil(remainingAvailableLengthHalf),
81-
endOffset = -1 * Math.floor(remainingAvailableLengthHalf),
82-
end = '';
83-
if (endOffset < 0) {
84-
end = segment.substr(endOffset);
85-
}
86-
return segment.substr(0, startOffset) + ellipsisChars + end;
87-
};
26+
// If the URL is shorter than the truncate length, return it as is
8827
if (url.length <= truncateLen) {
8928
return url;
9029
}
30+
9131
let availableLength = truncateLen - ellipsisLength;
92-
let urlObj = parse_url(url);
93-
// Clean up the URL
32+
let urlObj = parseUrl(url);
33+
34+
// Clean up the URL by removing any malformed query string
35+
// (e.g. "?foo=bar?ignorethis")
9436
if (urlObj.query) {
95-
let matchQuery = urlObj.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i);
37+
let matchQuery = urlObj.query.match(/^(.*?)(?=(\?|#))(.*?)$/i);
9638
if (matchQuery) {
9739
// Malformed URL; two or more "?". Removed any content behind the 2nd.
9840
urlObj.query = urlObj.query.substr(0, matchQuery[1].length);
9941
url = buildUrl(urlObj);
10042
}
10143
}
10244
if (url.length <= truncateLen) {
103-
return url;
45+
return url; // removing a malformed query string brought the URL under the truncateLength
10446
}
47+
48+
// Clean up the URL by removing 'www.' from the host if it exists
10549
if (urlObj.host) {
10650
urlObj.host = urlObj.host.replace(/^www\./, '');
10751
url = buildUrl(urlObj);
10852
}
10953
if (url.length <= truncateLen) {
110-
return url;
54+
return url; // removing 'www.' brought the URL under the truncateLength
11155
}
112-
// Process and build the URL
113-
let str = '';
56+
57+
// Process and build the truncated URL, starting with the hostname
58+
let truncatedUrl = '';
11459
if (urlObj.host) {
115-
str += urlObj.host;
60+
truncatedUrl += urlObj.host;
11661
}
117-
if (str.length >= availableLength) {
118-
if ((urlObj.host as string).length == truncateLen) {
119-
return (
120-
(urlObj.host as string).substr(0, truncateLen - ellipsisLength) + ellipsisChars
121-
).substr(0, availableLength + ellipsisLengthBeforeParsing);
62+
if (truncatedUrl.length >= availableLength) {
63+
if (urlObj.host!.length === truncateLen) {
64+
return (urlObj.host!.substr(0, truncateLen - ellipsisLength) + ellipsisChars).substr(
65+
0,
66+
availableLength + ellipsisLengthBeforeParsing
67+
);
12268
}
123-
return buildSegment(str, availableLength).substr(
69+
return buildSegment(truncatedUrl, availableLength, ellipsisChars).substr(
12470
0,
12571
availableLength + ellipsisLengthBeforeParsing
12672
);
12773
}
74+
75+
// If we still have available chars left, add the path and query string
12876
let pathAndQuery = '';
12977
if (urlObj.path) {
13078
pathAndQuery += '/' + urlObj.path;
@@ -133,53 +81,133 @@ export function truncateSmart(url: string, truncateLen: number, ellipsisChars?:
13381
pathAndQuery += '?' + urlObj.query;
13482
}
13583
if (pathAndQuery) {
136-
if ((str + pathAndQuery).length >= availableLength) {
137-
if ((str + pathAndQuery).length == truncateLen) {
138-
return (str + pathAndQuery).substr(0, truncateLen);
84+
if ((truncatedUrl + pathAndQuery).length >= availableLength) {
85+
if ((truncatedUrl + pathAndQuery).length == truncateLen) {
86+
return (truncatedUrl + pathAndQuery).substr(0, truncateLen);
13987
}
140-
let remainingAvailableLength = availableLength - str.length;
141-
return (str + buildSegment(pathAndQuery, remainingAvailableLength)).substr(
142-
0,
143-
availableLength + ellipsisLengthBeforeParsing
144-
);
88+
let remainingAvailableLength = availableLength - truncatedUrl.length;
89+
return (
90+
truncatedUrl + buildSegment(pathAndQuery, remainingAvailableLength, ellipsisChars)
91+
).substr(0, availableLength + ellipsisLengthBeforeParsing);
14592
} else {
146-
str += pathAndQuery;
93+
truncatedUrl += pathAndQuery;
14794
}
14895
}
96+
97+
// If we still have available chars left, add the fragment
14998
if (urlObj.fragment) {
15099
let fragment = '#' + urlObj.fragment;
151-
if ((str + fragment).length >= availableLength) {
152-
if ((str + fragment).length == truncateLen) {
153-
return (str + fragment).substr(0, truncateLen);
100+
if ((truncatedUrl + fragment).length >= availableLength) {
101+
if ((truncatedUrl + fragment).length == truncateLen) {
102+
return (truncatedUrl + fragment).substr(0, truncateLen);
154103
}
155-
let remainingAvailableLength2 = availableLength - str.length;
156-
return (str + buildSegment(fragment, remainingAvailableLength2)).substr(
157-
0,
158-
availableLength + ellipsisLengthBeforeParsing
159-
);
104+
let remainingAvailableLength2 = availableLength - truncatedUrl.length;
105+
return (
106+
truncatedUrl + buildSegment(fragment, remainingAvailableLength2, ellipsisChars)
107+
).substr(0, availableLength + ellipsisLengthBeforeParsing);
160108
} else {
161-
str += fragment;
109+
truncatedUrl += fragment;
162110
}
163111
}
112+
113+
// If we still have available chars left, add the scheme
164114
if (urlObj.scheme && urlObj.host) {
165115
let scheme = urlObj.scheme + '://';
166-
if ((str + scheme).length < availableLength) {
167-
return (scheme + str).substr(0, truncateLen);
116+
if ((truncatedUrl + scheme).length < availableLength) {
117+
return (scheme + truncatedUrl).substr(0, truncateLen);
168118
}
169119
}
170-
if (str.length <= truncateLen) {
171-
return str;
120+
if (truncatedUrl.length <= truncateLen) {
121+
return truncatedUrl;
172122
}
123+
173124
let end = '';
174125
if (availableLength > 0) {
175-
end = str.substr(-1 * Math.floor(availableLength / 2));
126+
end = truncatedUrl.substr(-1 * Math.floor(availableLength / 2));
176127
}
177-
return (str.substr(0, Math.ceil(availableLength / 2)) + ellipsisChars + end).substr(
128+
return (truncatedUrl.substr(0, Math.ceil(availableLength / 2)) + ellipsisChars + end).substr(
178129
0,
179130
availableLength + ellipsisLengthBeforeParsing
180131
);
181132
}
182133

134+
/**
135+
* Parses a URL into its components: scheme, host, path, query, and fragment.
136+
*/
137+
function parseUrl(url: string): UrlObject {
138+
// Functionality inspired by PHP function of same name
139+
let urlObj: UrlObject = {};
140+
let urlSub = url;
141+
142+
// Parse scheme
143+
let match = urlSub.match(/^([a-z]+):\/\//i);
144+
if (match) {
145+
urlObj.scheme = match[1];
146+
urlSub = urlSub.slice(match[0].length);
147+
}
148+
149+
// Parse host
150+
match = urlSub.match(/^(.*?)(?=(\?|#|\/|$))/i);
151+
if (match) {
152+
urlObj.host = match[1];
153+
urlSub = urlSub.slice(match[0].length);
154+
}
155+
156+
// Parse path
157+
match = urlSub.match(/^\/(.*?)(?=(\?|#|$))/i);
158+
if (match) {
159+
urlObj.path = match[1];
160+
urlSub = urlSub.slice(match[0].length);
161+
}
162+
163+
// Parse query
164+
match = urlSub.match(/^\?(.*?)(?=(#|$))/i);
165+
if (match) {
166+
urlObj.query = match[1];
167+
urlSub = urlSub.slice(match[0].length);
168+
}
169+
170+
// Parse fragment
171+
match = urlSub.match(/^#(.*?)$/i);
172+
if (match) {
173+
urlObj.fragment = match[1];
174+
//urlSub = urlSub.slice(match[0].length); -- not used. Uncomment if adding another block.
175+
}
176+
177+
return urlObj;
178+
}
179+
180+
function buildUrl(urlObj: UrlObject): string {
181+
let url = '';
182+
if (urlObj.scheme && urlObj.host) {
183+
url += urlObj.scheme + '://';
184+
}
185+
if (urlObj.host) {
186+
url += urlObj.host;
187+
}
188+
if (urlObj.path) {
189+
url += '/' + urlObj.path;
190+
}
191+
if (urlObj.query) {
192+
url += '?' + urlObj.query;
193+
}
194+
if (urlObj.fragment) {
195+
url += '#' + urlObj.fragment;
196+
}
197+
return url;
198+
}
199+
200+
function buildSegment(segment: string, remainingAvailableLength: number, ellipsisChars: string) {
201+
let remainingAvailableLengthHalf = remainingAvailableLength / 2,
202+
startOffset = Math.ceil(remainingAvailableLengthHalf),
203+
endOffset = -1 * Math.floor(remainingAvailableLengthHalf),
204+
end = '';
205+
if (endOffset < 0) {
206+
end = segment.substr(endOffset);
207+
}
208+
return segment.substr(0, startOffset) + ellipsisChars + end;
209+
}
210+
183211
interface UrlObject {
184212
scheme?: string;
185213
host?: string;

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,6 @@ export function removeWithPredicate<T>(arr: T[], fn: (item: T) => boolean) {
100100
* Function that should never be called but is used to check that every
101101
* enum value is handled using TypeScript's 'never' type.
102102
*/
103-
export function assertNever(theValue: never) {
103+
export function assertNever(theValue: never): never {
104104
throw new Error(`Unhandled case for value: '${theValue}'`);
105105
}

tests/truncate/truncate-smart.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ describe('Truncate.truncate.truncateSmart', function () {
2020
expect(truncatedUrl.length).toBe(72);
2121
});
2222

23+
it(`when just a hostname is present and it's exactly the truncate length, should return it as-is`, () => {
24+
let truncatedUrl = truncateSmart('yahoo.com', 'yahoo.com'.length, '..');
25+
26+
expect(truncatedUrl).toBe('yahoo.com');
27+
});
28+
29+
it(`when the hostname is exactly the truncate length but there's also a scheme, should ellipsis the hostname while removing the scheme`, () => {
30+
let truncatedUrl = truncateSmart('http://yahoo.com', 'yahoo.com'.length, '..');
31+
32+
expect(truncatedUrl).toBe('yahoo.c..');
33+
});
34+
2335
it('Will remove malformed query section 1st', function () {
2436
let truncatedUrl = truncateSmart(
2537
'http://www.yahoo.com/some/long/path/to/a/file?foo=bar?ignorethis#baz=bee',

0 commit comments

Comments
 (0)