Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/emdash-desktop/src/renderer/lib/external-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ describe('normalizeExternalHttpUrl', () => {
);
});

it('joins URLs split by line breaks', () => {
expect(normalizeExternalHttpUrl('https://example.com/long-\npath')).toBe(
'https://example.com/long-path'
);
expect(
normalizeExternalHttpUrl(
'https://uploads.linear.app/d402fa9e-639b-4d1b-837a-c271947359e6/92132495-d5d\n 4-416f-806a-b47089d82878/ad04fc4b-50c0-4770-add5-2d182feb1d24'
)
).toBe(
'https://uploads.linear.app/d402fa9e-639b-4d1b-837a-c271947359e6/92132495-d5d4-416f-806a-b47089d82878/ad04fc4b-50c0-4770-add5-2d182feb1d24'
);
});

it('does not join unrelated text on the next line', () => {
expect(normalizeExternalHttpUrl('https://example.com/path\nStatus: Todo')).toBe(
'https://example.com/path'
);
expect(normalizeExternalHttpUrl('https://example.com/path\n Status: Todo')).toBe(
'https://example.com/path'
);
});
Comment thread
janburzinski marked this conversation as resolved.

it('removes trailing punctuation commonly printed after URLs', () => {
expect(normalizeExternalHttpUrl('https://example.com/path,')).toBe('https://example.com/path');
expect(normalizeExternalHttpUrl('https://example.com/path).')).toBe('https://example.com/path');
Expand Down
34 changes: 30 additions & 4 deletions apps/emdash-desktop/src/renderer/lib/external-url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const TRAILING_URL_CHARS_PATTERN = /[,.!?;:'"<>}\]]+$/;
const URL_CONTINUATION_CHARS_PATTERN = /[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=%]/;

function hasExtraClosingParen(value: string): boolean {
let balance = 0;
Expand All @@ -16,10 +17,8 @@ function hasExtraClosingParen(value: string): boolean {

export function normalizeExternalHttpUrl(value: string): string {
let url = value.trim();
const trailingTextIndex = url.search(/\s/);
if (trailingTextIndex !== -1) {
url = url.slice(0, trailingTextIndex);
}
url = joinMultilineUrl(url);
url = trimTrailingText(url);
url = url.replace(TRAILING_URL_CHARS_PATTERN, '');

while (url.endsWith(')') && hasExtraClosingParen(url)) {
Expand All @@ -28,3 +27,30 @@ export function normalizeExternalHttpUrl(value: string): string {

return url;
}

function joinMultilineUrl(value: string): string {
return value.replace(/\r?\n[ \t]*/g, (lineBreak, offset) => {
const previousChar = value[offset - 1];
const nextChar = value[offset + lineBreak.length];
if (!previousChar || !nextChar || !URL_CONTINUATION_CHARS_PATTERN.test(nextChar)) {
return lineBreak;
}

const hasIndent = /[ \t]$/.test(lineBreak);
const breaksUrlSegment = /[-._~:/?#[\]@!$&'()*+,;=%]/.test(previousChar);
const startsIndentedLabel = /^[A-Za-z][A-Za-z0-9_-]*:/.test(
value.slice(offset + lineBreak.length)
);

if (hasIndent && !breaksUrlSegment && startsIndentedLabel) {
return lineBreak;
}

return hasIndent || breaksUrlSegment ? '' : lineBreak;
Comment thread
janburzinski marked this conversation as resolved.
});
}

function trimTrailingText(value: string): string {
const trailingTextIndex = value.search(/[ \t\r\n]/);
return trailingTextIndex === -1 ? value : value.slice(0, trailingTextIndex);
}
Loading