Skip to content

Commit

Permalink
(chore) Refactor the code around parsed and entry (#52)
Browse files Browse the repository at this point in the history
* (chore) Refactor the code around parsed and entry

This commit changes a couple of things to make both apis be more
similar.

There was an error in parsed where toSchemeLess would return a
protocol-less version. This is fixed.

toRelative does not make sense without a 'base' url (relative to what?).
node's path.relative requires 2 params.
So it is now deprecated

4 new functions, across 3 new files were created to avoid code dup.
2 of them are exposed in the barrel file, since they work on strings.
The other two are not since they require a valid URL object, which is
not validated, hence, not exposed.

Some improvement, IMHO, to the docs.

* (chore) Add cs

* Fix typo

Co-authored-by: Felix Guerin <[email protected]>

* Fix typo

Co-authored-by: Felix Guerin <[email protected]>

---------

Co-authored-by: Felix Guerin <[email protected]>
  • Loading branch information
nitriques and f-elix authored Feb 12, 2025
1 parent c7686a2 commit 2fc065b
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-pets-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@288-toolkit/url': minor
---

Refactor the code around parsed and entry
18 changes: 9 additions & 9 deletions packages/url/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@ Validate if the URL is from the same origin as the request URL.

## `parsedUrl()`

Safely parses a URL and expose and nice API to access the parts of the URL.
If the URL is not valid, all functions returns null.
Safely parses a URL and exposes a nice API to access the parts of the URL. If the URL is not valid,
all functions return null.

This should be used instead of the `URL` constructor, since it can throw errors.

## `createEntryUrlBuilder`

Creates a function that builds URLs for entries.
Its api is similar to the `parsedUrl` function.
Creates a function that builds URLs for entries. Its api is similar to the `parsedUrl` function, but
is requires global options, so a builder function is exposed.

```ts
const getEntryUrl = createEntryUrlBuilder({
const entryUrl = createEntryUrlBuilder({
siteUrl: 'https://example.org',
shouldRemoveTrailingSlash: true
});

getEntryUrl(mockEntry).raw; // The URL object.
getEntryUrl(mockEntry).toAbsolute(); // Returns the full URL string.
getEntryUrl(mockEntry).toString(); // Returns the full URL string.
getEntryUrl(mockEntry).toSchemeLess(); // Returns the URL string without the scheme, composed of the pathname, search, and hash.
entryUrl({ url: '...' }).toAbsolute(); // Returns the full URL string.
entryUrl({ url: '...' }).toSchemeLess(); // Returns the URL string without the scheme, composed of the pathname, search, and hash.
```

## `urlCanParse`
Expand Down
24 changes: 16 additions & 8 deletions packages/url/src/createEntryUrlBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { normalize, removeTrailingSlash } from '@288-toolkit/strings';
import { removeTrailingSlash } from '@288-toolkit/strings';
import type { Maybe } from '@288-toolkit/types';
import { decodePath } from './decodePath.js';
import { protocolLessUrl, schemeLessUrl } from './lessUrl.js';
import { normalizePath } from './normalizePath.js';
import { urlCanParse } from './urlCanParse.js';

export type Entry = {
Expand Down Expand Up @@ -50,6 +53,11 @@ export interface EntryUrl {
*/
toAbsolute(): Maybe<string>;

/**
* Returns the URL string without the protocol, composed of the hostname, pathname, search, and hash.
*/
toProtocolLess(): Maybe<string>;

/**
* Returns the URL string without the scheme, composed of the pathname, search, and hash.
*/
Expand Down Expand Up @@ -83,6 +91,7 @@ export const createEntryUrlBuilder = ({
decodedPath: () => '',
normalizePath: () => empty,
toAbsolute: () => null,
toProtocolLess: () => null,
toSchemeLess: () => null,
/** @deprecated Use `toSchemeLess` instead. */
toLanguageRelative: () => null,
Expand All @@ -103,13 +112,10 @@ export const createEntryUrlBuilder = ({
const self = {
raw: url,
decodedPath() {
return url.pathname.split('/').map(decodeURIComponent).join('/');
return decodePath(url.pathname);
},
normalizePath() {
url.pathname = url.pathname
.split('/')
.map((part) => normalize(decodeURIComponent(part)))
.join('/');
url.pathname = normalizePath(url.pathname);
return self;
},
toString() {
Expand All @@ -118,9 +124,11 @@ export const createEntryUrlBuilder = ({
toAbsolute() {
return url.toString();
},
toProtocolLess() {
return protocolLessUrl(url);
},
toSchemeLess() {
const { pathname, hash, search } = url;
return `${pathname}${search}${hash}`;
return schemeLessUrl(url);
},
/** @deprecated Use `toSchemeLess` instead. */
toLanguageRelative() {
Expand Down
8 changes: 8 additions & 0 deletions packages/url/src/decodePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Decodes the pathname by splitting it into parts, decoding each part, and joining them with slashes.
* @param pathname - The pathname to decode.
* @returns The decoded pathname.
*/
export const decodePath = (pathname: string) => {
return pathname.split('/').map(decodeURIComponent).join('/');
};
2 changes: 1 addition & 1 deletion packages/url/src/getLanguageRelativeUri.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Remove the home URI from a URI.
* @deprecated Use createEntryUrlBuilder instead.
* @deprecated This was used with Craft 4. With Craft 5, use createEntryUrlBuilder instead.
*/
export const getLanguageRelativeUri = (uri: string, homeUri: string) =>
uri?.replace(homeUri, '') || '';
Expand Down
4 changes: 3 additions & 1 deletion packages/url/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './createEntryUrlBuilder.js';
export * from './decodePath.js';
export * from './isExternalUrl.js';
export * from './validateSameOrigin.js';
export * from './normalizePath.js';
export * from './parsedUrl.js';
export * from './urlCanParse.js';
export * from './validateSameOrigin.js';
19 changes: 19 additions & 0 deletions packages/url/src/lessUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Returns the URL string without the protocol, composed of the hostname, pathname, search, and hash.
* It starts with a double slash.
* @param url - The URL to convert.
*/
export const protocolLessUrl = (url: URL) => {
const { hostname, pathname, hash, search } = url;
return `//${hostname}${pathname}${search}${hash}`;
};

/**
* Returns the URL string without the scheme, composed of the pathname, search, and hash.
* It starts with a single slash.
* @param url - The URL to convert.
*/
export const schemeLessUrl = (url: URL) => {
const { pathname, hash, search } = url;
return `${pathname}${search}${hash}`;
};
14 changes: 14 additions & 0 deletions packages/url/src/normalizePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { normalize } from '@288-toolkit/strings';

/**
* Normalizes the pathname by decoding it, normalizing each part, and joining them with slashes.
* @see {@link @288-toolkit/strings#normalize}
* @param pathname - The pathname to normalize.
* @returns The normalized pathname.
*/
export const normalizePath = (pathname: string) => {
return pathname
.split('/')
.map((part) => normalize(decodeURIComponent(part)))
.join('/');
};
47 changes: 36 additions & 11 deletions packages/url/src/parsedUrl.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { normalize } from '@288-toolkit/strings';
import { decodePath } from './decodePath.js';
import { protocolLessUrl, schemeLessUrl } from './lessUrl.js';
import { normalizePath } from './normalizePath.js';
import { urlCanParse } from './urlCanParse.js';

/**
* Safely parses a URL and expose and nice API to access the parts of the URL.
* If the URL is not valid, all functions returns null.
*/
export const parsedUrl = (url: string | URL) => {
const parsed = urlCanParse(url) ? new URL(url) : null;
const parsed = urlCanParse(url.toString()) ? new URL(url) : null;

const api = {
/**
* The URL object.
*/
parsed,
/**
* Checks if the URL is valid.
*/
valid: () => {
return parsed != null && parsed.toString() !== '';
},
/**
* Makes sure the pathname is properly encoded.
* This can be needed when the pathname contains special characters.
*
* @deprecated Pathname are always url encoded.
*/
encodePath: () => {
if (!parsed) {
Expand All @@ -27,15 +34,25 @@ export const parsedUrl = (url: string | URL) => {
parsed.pathname = encodeURIComponent(parsed.pathname);
return api;
},
/**
* Decodes the pathname and returns it.
* It can't be stored in the URL object because it would be encoded again.
*/
decodePath: () => {
if (!parsed) {
return null;
}
return decodePath(parsed.pathname);
},
/**
* Normalizes the pathname by removing accents.
* @see {@link @288-toolkit/strings#normalize}
*/
normalizePath: () => {
if (!parsed) {
return null;
return api;
}
parsed.pathname = normalize(parsed.pathname);
parsed.pathname = normalizePath(parsed.pathname);
return api;
},
/**
Expand All @@ -53,31 +70,39 @@ export const parsedUrl = (url: string | URL) => {
/**
* Returns the relative URL string.
* It starts with a single slash.
* @deprecated Relative URLs do not start with a single slash.
* They are relative to the current URL.
* Use `toSchemeLess()` instead.
*/
toRelative: () => {
return api.toSchemeLess();
},
/**
* Returns the URL string without the scheme, composed of the pathname, search, and hash.
* It starts with a single slash.
*/
toSchemeLess: () => {
if (!parsed) {
return null;
}
const { pathname, hash, search } = parsed;
return `${pathname}${search}${hash}`;
return schemeLessUrl(parsed);
},
/**
* Returns the URL string without the scheme, composed of the pathname, search, and hash.
* Returns the URL string without the protocol, composed of the hostname, pathname, search, and hash.
* It starts with a double slash.
*/
toSchemeLess: () => {
toProtocolLess: () => {
if (!parsed) {
return null;
}
const { hostname, pathname, hash, search } = parsed;
return `//${hostname}${pathname}${search}${hash}`;
return protocolLessUrl(parsed);
},
parts: () => {
if (!parsed) {
return null;
}
return parsed.pathname.split('/').filter(Boolean);
},
}
};

return api;
Expand Down
11 changes: 9 additions & 2 deletions packages/url/test/parsedUrl.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { parsedUrl } from "../src/parsedUrl.js";
import { parsedUrl } from '../src/parsedUrl.js';

describe('`parsed` property should return the url object', () => {
test('localized', () => {
Expand Down Expand Up @@ -40,9 +40,16 @@ describe('`toAbsolute` method should return the absolute url string', () => {
});
});

describe('`toProtocolLess` method should return the url string without the protocol', () => {
test('valid', () => {
const url = parsedUrl('https://example.com/path/to/resource');
expect(url.toProtocolLess()).toBe('//example.com/path/to/resource');
});
});

describe('`toSchemeLess` method should return the url string without the scheme', () => {
test('valid', () => {
const url = parsedUrl('https://example.com/path/to/resource');
expect(url.toSchemeLess()).toBe('//example.com/path/to/resource');
expect(url.toSchemeLess()).toBe('/path/to/resource');
});
});

0 comments on commit 2fc065b

Please sign in to comment.