Skip to content

Commit 5f4fe0c

Browse files
committed
refactor: remove hardcoded path
1 parent e83c3e0 commit 5f4fe0c

File tree

6 files changed

+93
-56
lines changed

6 files changed

+93
-56
lines changed

website/src/components/templates/BaseTemplate.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,11 @@ export const BaseTemplate: FC<BaseTemplateProps> = ({
243243
</a>
244244
)}
245245

246-
{route === "/docs/" ? (
246+
{route === basePath ? (
247247
<div class="doc-categories grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
248248
<a
249249
class="doc-category flex flex-col p-6 bg-white border border-gray-200 rounded-lg hover:border-gray-500 hover:bg-gray-50 transition-all duration-200"
250-
href="/docs/tutorial"
250+
href={joinPath(basePath, "/tutorial/")}
251251
>
252252
<div class="flex items-center mb-3">
253253
<div class="w-6 h-6 text-gray-800 mr-2">
@@ -263,7 +263,7 @@ export const BaseTemplate: FC<BaseTemplateProps> = ({
263263
</a>
264264
<a
265265
class="doc-category flex flex-col p-6 bg-white border border-gray-200 rounded-lg hover:border-gray-500 hover:bg-gray-50 transition-all duration-200"
266-
href="/docs/reference"
266+
href={joinPath(basePath, "/reference/")}
267267
>
268268
<div class="flex items-center mb-3">
269269
<div class="w-6 h-6 text-gray-800 mr-2">

website/src/components/ui/FunctionDefinition.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { FC } from "hono/jsx";
2+
import { basePath } from "../../metadata";
23
import type { Func } from "../../types/model";
4+
import { joinPath } from "../../utils/path";
35
import { TypeIcon } from "./TypeIcon";
46
import { genPath } from "./genPath";
57
import { buildParamId, type2href } from "./type2href";
@@ -48,7 +50,9 @@ export const FunctionDefinition: FC<FunctionDefinitionProps> = ({
4850
<TypeIcon
4951
key={t}
5052
type={t}
51-
href={href ? `/docs/reference/${href}` : undefined}
53+
href={
54+
href ? joinPath(basePath, "reference", href) : undefined
55+
}
5256
/>
5357
);
5458
})}
@@ -72,7 +76,9 @@ export const FunctionDefinition: FC<FunctionDefinitionProps> = ({
7276
<TypeIcon
7377
key={ret}
7478
type={ret}
75-
href={href ? `/docs/reference/${href}` : undefined}
79+
href={
80+
href ? joinPath(basePath, "reference", href) : undefined
81+
}
7682
/>
7783
{i < func.returns.length - 1 && (
7884
<span class="text-gray-500 mx-1">,</span>

website/src/components/ui/FunctionParameters.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { FC } from "hono/jsx";
2+
import { basePath } from "../../metadata";
23
import type { Func } from "../../types/model";
4+
import { joinPath } from "../../utils/path";
35
import { ChevronRightIcon } from "../icons";
46
import { HtmlContent } from "./HtmlContent";
57
import { Tooltip } from "./Tooltip";
@@ -40,7 +42,9 @@ export const FunctionParameters: FC<FunctionParametersProps> = ({
4042
<TypeIcon
4143
key={t}
4244
type={t}
43-
href={href ? `/docs/reference/${href}` : undefined}
45+
href={
46+
href ? joinPath(basePath, "reference", href) : undefined
47+
}
4448
/>
4549
);
4650
})}

website/src/components/ui/type2href.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ export const type2href = (parameterType: string): string | null => {
4545
const introspectionSet = new Set(["counter", "location", "state"]);
4646

4747
if (foundationSet.has(parameterType)) {
48-
return `foundations/${parameterType}`;
48+
return `foundations/${parameterType}/`;
4949
}
5050
if (layoutSet.has(parameterType)) {
51-
return `layout/${parameterType}`;
51+
return `layout/${parameterType}/`;
5252
}
5353
if (visualizeSet.has(parameterType)) {
54-
return `visualize/${parameterType}`;
54+
return `visualize/${parameterType}/`;
5555
}
5656
if (introspectionSet.has(parameterType)) {
57-
return `introspection/${parameterType}`;
57+
return `introspection/${parameterType}/`;
5858
}
5959
return null;
6060
};

website/src/utils/path.test.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { applyBasePath, joinPath, removeBasePath } from "./path";
2+
import { joinPath, removeBasePath } from "./path";
33

44
describe("joinPath", () => {
55
it("should join base and path with single slash", () => {
@@ -24,25 +24,58 @@ describe("joinPath", () => {
2424
expect(joinPath("", "/foo")).toBe("/foo");
2525
expect(joinPath("", "foo")).toBe("/foo");
2626
});
27-
});
2827

29-
describe("applyBasePath", () => {
30-
it("should apply basePath to absolute path", () => {
31-
expect(applyBasePath("/base", "/foo")).toBe("/base/foo");
32-
expect(applyBasePath("/foo", "/bar/baz")).toBe("/foo/bar/baz");
28+
it("should join multiple parts", () => {
29+
expect(joinPath("/base", "foo", "bar")).toBe("/base/foo/bar");
30+
expect(joinPath("/", "foo", "bar")).toBe("/foo/bar");
31+
expect(joinPath("", "foo", "bar")).toBe("/foo/bar");
32+
expect(joinPath("/base/", "/foo/", "/bar/")).toBe("/base/foo/bar/");
3333
});
3434

35-
it("should not apply basePath to relative path", () => {
36-
expect(applyBasePath("/base", "./index.html")).toBe("./index.html");
37-
expect(applyBasePath("/foo/bar", "baz/qux")).toBe("baz/qux");
35+
it("should handle parts with trailing and leading slashes", () => {
36+
expect(joinPath("/base/", "/foo/", "/bar/")).toBe("/base/foo/bar/");
37+
expect(joinPath("/base/", "foo/", "/bar")).toBe("/base/foo/bar");
38+
expect(joinPath("/base", "foo/", "bar/")).toBe("/base/foo/bar/");
3839
});
3940

40-
it("should handle root basePath", () => {
41-
expect(applyBasePath("/", "/foo")).toBe("/foo");
41+
it("should handle parts with only slashes", () => {
42+
expect(joinPath("/", "/", "/")).toBe("/");
43+
expect(joinPath("/", "", "/foo")).toBe("/foo");
4244
});
4345

44-
it("should handle empty basePath", () => {
45-
expect(applyBasePath("", "/foo")).toBe("/foo");
46+
it("should preserve trailing slash if last part has it", () => {
47+
expect(joinPath("/base", "foo/", "bar/")).toBe("/base/foo/bar/");
48+
expect(joinPath("/base", "foo", "bar/")).toBe("/base/foo/bar/");
49+
});
50+
51+
it("should handle special paths with leading slash", () => {
52+
expect(joinPath("/base", "/@")).toBe("/base/@");
53+
expect(joinPath("/base", "/node_modules")).toBe("/base/node_modules");
54+
});
55+
56+
it("should remove duplicate slashes safely", () => {
57+
expect(joinPath("/base//", "/foo//bar/")).toBe("/base/foo/bar/");
58+
expect(joinPath("//", "/foo//", "/bar//baz//")).toBe("/foo/bar/baz/");
59+
expect(joinPath("/", "/", "/foo//bar//baz/")).toBe("/foo/bar/baz/");
60+
expect(joinPath("/base//", "//foo//", "//bar//")).toBe("/base/foo/bar/");
61+
expect(joinPath("base//", "/foo//bar/")).toBe("base/foo/bar/");
62+
expect(joinPath("base//", "foo//bar/")).toBe("base/foo/bar/");
63+
expect(joinPath("/base//", "foo//bar")).toBe("/base/foo/bar");
64+
expect(joinPath("base//", "foo//bar")).toBe("base/foo/bar");
65+
});
66+
67+
it("should handle only slash and empty", () => {
68+
expect(joinPath("/")).toBe("/");
69+
expect(joinPath("", "/")).toBe("/");
70+
expect(joinPath("", "")).toBe("");
71+
});
72+
73+
it("should handle mix of empty, slash, and normal parts", () => {
74+
expect(joinPath("", "/", "foo")).toBe("/foo");
75+
expect(joinPath("", "foo", "")).toBe("/foo/");
76+
expect(joinPath("", "foo", "/")).toBe("/foo/");
77+
expect(joinPath("/", "", "foo")).toBe("/foo");
78+
expect(joinPath("/", "foo", "")).toBe("/foo/");
4679
});
4780
});
4881

website/src/utils/path.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
2-
* Joins two paths by resolving any slash collisions between them.
2+
* Join multiple URL path segments safely.
33
*
4-
* @param basePath - The base path to join.
5-
* @param path - The path to append to the basePath.
4+
* @param parts - The path segments to join.
65
* @returns The joined path with redundant slashes resolved.
76
*
87
* @example
@@ -12,39 +11,34 @@
1211
* joinPath("/base", "/foo/bar") -> "/base/foo/bar"
1312
* joinPath("/foo/bar/", "/baz/qux") -> "/foo/bar/baz/qux"
1413
* joinPath("/foo/bar", "baz/qux") -> "/foo/bar/baz/qux"
14+
* joinPath("/base", "foo", "bar") -> "/base/foo/bar"
15+
* joinPath("/", "foo", "bar") -> "/foo/bar"
1516
* ```
1617
*/
17-
export const joinPath = (basePath: string, path: string): string => {
18-
// Remove trailing slash from base.
19-
const baseClean = basePath !== "/" ? basePath.replace(/\/+$/, "") : basePath;
20-
// Remove leading slash from path.
21-
const pathClean = path.replace(/^\/+/, "");
22-
// Special case: if base is '/', avoid double slash
23-
if (baseClean === "/") {
24-
return `/${pathClean}`;
25-
}
26-
return `${baseClean}/${pathClean}`;
27-
};
18+
export const joinPath = (...parts: string[]): string => {
19+
if (parts.length === 0) return "";
20+
if (parts.length === 1) return parts[0];
21+
if (parts.every((p) => !p)) return "";
2822

29-
/**
30-
* Applies a base path to internal non-relative URLs (starting with `/`).
31-
* If the path is relative, it is returned as-is.
32-
*
33-
* @param basePath - The base path to apply.
34-
* @param path - The original path to modify.
35-
* @returns The modified path with the basePath applied if it was an absolute path.
36-
*
37-
* @example
38-
* ```ts
39-
* applyBasePath("/base", "/foo") -> "/base/foo"
40-
* applyBasePath("/base", "./index.html") -> "./index.html"
41-
* applyBasePath("/foo", "/bar/baz") -> "/foo/bar/baz"
42-
* applyBasePath("/foo/bar", "baz/qux") -> "baz/qux"
43-
* ```
44-
*/
45-
export const applyBasePath = (basePath: string, path: string): string => {
46-
const isRelative = !path.startsWith("/");
47-
return isRelative ? path : joinPath(basePath, path);
23+
let needsLeadingSlash = false;
24+
const firstNonEmpty = parts.find((p) => p !== "") || "";
25+
if (parts[0].startsWith("/")) {
26+
needsLeadingSlash = true;
27+
} else if (parts[0] === "") {
28+
if (firstNonEmpty?.startsWith("/")) needsLeadingSlash = true;
29+
}
30+
let needsTrailingSlash = false;
31+
if (parts[parts.length - 1].endsWith("/")) needsTrailingSlash = true;
32+
const cleaned = parts.map((p, i) => {
33+
if (i === 0) return p.replace(/\/+$/, "");
34+
if (i === parts.length - 1) return p.replace(/^\/+/, "");
35+
return p.replace(/^\/+|\/+$/g, "");
36+
});
37+
let joined = cleaned.filter((v, i) => v !== "" || parts[i] === "").join("/");
38+
if (needsLeadingSlash && !joined.startsWith("/")) joined = `/${joined}`;
39+
if (needsTrailingSlash && !joined.endsWith("/")) joined = `${joined}/`;
40+
joined = joined.replace(/\/+/g, "/");
41+
return joined;
4842
};
4943

5044
/**

0 commit comments

Comments
 (0)