Skip to content

Commit

Permalink
feat: joinRelativeURL (#220)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 15, 2024
1 parent 3aaf64d commit 69e26f8
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 21 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ isSamePath("/foo", "/foo/"); // true

Checks if the input protocol is any of the dangerous `blob:`, `data:`, `javascript`: or `vbscript:` protocols.

### `joinRelativeURL()`

Joins multiple URL segments into a single URL and also handles relative paths with `./` and `../`.

**Example:**

```js
joinRelativeURL("/a", "../b", "./c"); // "/b/c"
```

### `joinURL(base)`

Joins multiple URL segments into a single URL.
Expand Down
30 changes: 29 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const PROTOCOL_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{2})?/;
const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/;
const PROTOCOL_SCRIPT_RE = /^[\s\0]*(blob|data|javascript|vbscript):$/i;
const TRAILING_SLASH_RE = /\/$|\/\?|\/#/;
const JOIN_LEADING_SLASH_RE = /^\.?\//;

/**
* Check if a path starts with `./` or `../`.
Expand Down Expand Up @@ -317,7 +318,34 @@ export function isNonEmptyURL(url: string) {
*
* @group utils
*/
export function joinURL(..._input: string[]): string {
export function joinURL(base: string, ...input: string[]): string {
let url = base || "";

for (const segment of input.filter((url) => isNonEmptyURL(url))) {
if (url) {
// TODO: Handle .. when joining
const _segment = segment.replace(JOIN_LEADING_SLASH_RE, "");
url = withTrailingSlash(url) + _segment;
} else {
url = segment;
}
}

return url;
}

/**
* Joins multiple URL segments into a single URL and also handles relative paths with `./` and `../`.
*
* @example
*
* ```js
* joinRelativeURL("/a", "../b", "./c"); // "/b/c"
* ```
*
* @group utils
*/
export function joinRelativeURL(..._input: string[]): string {
const input = _input.filter(Boolean);

const segments: string[] = [];
Expand Down
2 changes: 1 addition & 1 deletion test/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe("withBase", () => {
const tests = [
{ base: "/", input: "/", out: "/" },
{ base: "/foo", input: "", out: "/foo" },
{ base: "/foo/", input: "/", out: "/foo/" },
{ base: "/foo/", input: "/", out: "/foo" },
{ base: "/foo", input: "/bar", out: "/foo/bar" },
{ base: "/base/", input: "/base", out: "/base" },
{ base: "/base", input: "/base/", out: "/base/" },
Expand Down
49 changes: 30 additions & 19 deletions test/join.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { describe, expect, test } from "vitest";
import { joinURL } from "../src";
import { joinURL, joinRelativeURL } from "../src";

const joinURLTests = [
{ input: [], out: "" },
{ input: ["/"], out: "/" },
{ input: [undefined, "./"], out: "./" },
{ input: ["./", "a"], out: "./a" },
{ input: ["./a", "./b"], out: "./a/b" },
{ input: ["/a"], out: "/a" },
{ input: ["a", "b"], out: "a/b" },
{ input: ["/", "/b"], out: "/b" },
{ input: ["a", "b/", "c"], out: "a/b/c" },
{ input: ["a", "b/", "/c"], out: "a/b/c" },
{ input: ["/", "./"], out: "/" },
{ input: ["/", "./foo"], out: "/foo" },
{ input: ["/", "./foo/"], out: "/foo/" },
{ input: ["/", "./foo", "bar"], out: "/foo/bar" },
] as const;

describe("joinURL", () => {
const tests = [
{ input: [], out: "" },
{ input: ["/"], out: "/" },
{ input: [undefined, "./"], out: "./" },
{ input: ["./", "a"], out: "./a" },
{ input: ["./a", "./b"], out: "./a/b" },
{ input: ["/a"], out: "/a" },
{ input: ["a", "b"], out: "a/b" },
{ input: ["/", "/b"], out: "/b" },
{ input: ["a", "b/", "c"], out: "a/b/c" },
{ input: ["a", "b/", "/c"], out: "a/b/c" },
{ input: ["/", "./"], out: "/" },
{ input: ["/", "./foo"], out: "/foo" },
{ input: ["/", "./foo/"], out: "/foo/" },
{ input: ["/", "./foo", "bar"], out: "/foo/bar" },
for (const t of joinURLTests) {
test(`joinURL(${t.input.map((i) => JSON.stringify(i)).join(", ")}) === ${JSON.stringify(t.out)}`, () => {
expect(joinURL(...(t.input as any[]))).toBe(t.out);
});
}
});

describe("joinRelativeURL", () => {
const relativeTests = [
...joinURLTests,
// Relative with ../
{ input: ["/a", "../b"], out: "/b" },
{ input: ["/a/b/c", "../../d"], out: "/a/d" },
Expand All @@ -33,9 +44,9 @@ describe("joinURL", () => {
{ input: ["../a/", "../b"], out: "b" },
];

for (const t of tests) {
test(`joinURL(${t.input.map((i) => JSON.stringify(i)).join(", ")}) === ${JSON.stringify(t.out)}`, () => {
expect(joinURL(...(t.input as string[]))).toBe(t.out);
for (const t of relativeTests) {
test(`joinRelativeURL(${t.input.map((i) => JSON.stringify(i)).join(", ")}) === ${JSON.stringify(t.out)}`, () => {
expect(joinRelativeURL(...(t.input as string[]))).toBe(t.out);
});
}
});

0 comments on commit 69e26f8

Please sign in to comment.