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
7 changes: 4 additions & 3 deletions website/src/components/templates/BaseTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FC, PropsWithChildren } from "hono/jsx";
import { basePath, originUrl, typstOfficialDocsUrl } from "../../metadata";
import { Translation, translation } from "../../translation/";
import type { Page } from "../../types/model";
import { joinPath, removeBasePath } from "../../utils/path";
import { joinPath, shiftBase } from "../../utils/path";
import { getTranslationStatus } from "../../utils/translationStatus";
import {
CaretRightCircleIcon,
Expand Down Expand Up @@ -50,9 +50,10 @@ export const BaseTemplate: FC<BaseTemplateProps> = ({
joinPath(basePath, "/favicon.png"),
originUrl,
).toString();
const typstOfficialRouteUrl = joinPath(
const typstOfficialRouteUrl = shiftBase(
route,
basePath,
typstOfficialDocsUrl,
removeBasePath(basePath, route),
);
return (
<html lang={translation.htmlLang()} class="scroll-pt-24">
Expand Down
4 changes: 3 additions & 1 deletion website/src/components/templates/SymbolsTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { FC } from "hono/jsx";
import { basePath, typstOfficialDocsUrl } from "../../metadata";
import type { Page, SymbolsBody } from "../../types/model";
import { shiftBase } from "../../utils/path";
import type { BaseTemplateProps } from "./BaseTemplate";

export type SymbolsTemplateProps = Omit<BaseTemplateProps, "page"> & {
Expand All @@ -9,7 +11,7 @@ export type SymbolsTemplateProps = Omit<BaseTemplateProps, "page"> & {
};

export const SymbolsTemplate: FC<SymbolsTemplateProps> = ({ page }) => {
const redirectUrl = `https://typst.app${page.route}`;
const redirectUrl = shiftBase(page.route, basePath, typstOfficialDocsUrl);

return (
<html lang="ja">
Expand Down
153 changes: 152 additions & 1 deletion website/src/utils/path.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { joinPath, removeBasePath } from "./path";
import { joinPath, removeBasePath, shiftBase } from "./path";

describe("joinPath", () => {
it("should join base and path with single slash", () => {
Expand Down Expand Up @@ -119,3 +119,154 @@ describe("removeBasePath", () => {
expect(removeBasePath("", "/foo/bar")).toBe("/foo/bar");
});
});

describe("shiftBase", () => {
describe("normal usages", () => {
it("should handle non-root oldBasePath and full newBaseUrl", () => {
for (const oldBasePath of ["/base", "/base/"]) {
for (const newBaseUrl of [
"https://typst.app/docs",
"https://typst.app/docs/",
]) {
expect(shiftBase("/base/foo/bar/", oldBasePath, newBaseUrl)).toBe(
"https://typst.app/docs/foo/bar/",
);
expect(shiftBase("/base/", oldBasePath, newBaseUrl)).toBe(
"https://typst.app/docs/",
);
}
}
});

it("should handle root oldBasePath and full newBaseUrl", () => {
for (const oldBasePath of ["/", ""]) {
for (const newBaseUrl of [
"https://typst.app/docs",
"https://typst.app/docs/",
]) {
expect(shiftBase("/foo/bar/", oldBasePath, newBaseUrl)).toBe(
"https://typst.app/docs/foo/bar/",
);
expect(shiftBase("/", oldBasePath, newBaseUrl)).toBe(
"https://typst.app/docs/",
);
}
}
});

it("should handle non-root oldBasePath and newBaseUrl without origin", () => {
for (const oldBasePath of ["/ja-JP/docs", "/ja-JP/docs/"]) {
for (const newBaseUrl of ["/en-US/docs", "/en-US/docs/"]) {
expect(
shiftBase("/ja-JP/docs/foo/bar/", oldBasePath, newBaseUrl),
).toBe("/en-US/docs/foo/bar/");
expect(shiftBase("/ja-JP/docs/", oldBasePath, newBaseUrl)).toBe(
"/en-US/docs/",
);
}
}
});
});

// The following tests are generated by AI automatically. They describe the behaviors in edge cases.
// However, these behaviors are not actually used, and their result and may not meet actual needs.
// Therefore, the following usages should be avoided in practice.
describe("generated usages", () => {
it("should handle edge cases with empty oldBasePath and newBaseUrl", () => {
expect(shiftBase("/foo/bar", "", "")).toBe("/foo/bar");
expect(shiftBase("/", "", "")).toBe("/");
expect(shiftBase("", "", "")).toBe("");
});

it("should handle edge cases with empty route", () => {
expect(shiftBase("", "/base", "/new")).toBe("/new/");
expect(shiftBase("", "/", "/new")).toBe("/new/");
expect(shiftBase("", "", "/new")).toBe("/new/");
});

it("should handle edge cases with only slashes", () => {
expect(shiftBase("/", "/", "/new")).toBe("/new/");
expect(shiftBase("/", "/base", "/new")).toBe("/new/");
expect(shiftBase("/base", "/base", "/new")).toBe("/new/");
});

it("should handle routes with duplicate slashes", () => {
expect(shiftBase("/base//foo//bar", "/base", "/new")).toBe(
"/new/foo/bar",
);
expect(shiftBase("//base//foo//", "/base", "/new")).toBe(
"/new/base/foo/",
);
expect(shiftBase("//base//", "/base", "/new")).toBe("/new/base/");
});

it("should handle routes with special characters", () => {
expect(shiftBase("/base/@/foo", "/base", "/new")).toBe("/new/@/foo");
expect(shiftBase("/base/#/foo", "/base", "/new")).toBe("/new/#/foo");
expect(shiftBase("/base/!$/foo", "/base", "/new")).toBe("/new/!$/foo");
});

it("should handle routes with trailing slashes in newBaseUrl", () => {
expect(shiftBase("/base/foo", "/base", "/new/")).toBe("/new/foo");
expect(shiftBase("/base/foo/", "/base", "/new/")).toBe("/new/foo/");
expect(shiftBase("/base/", "/base", "/new/")).toBe("/new/");
});

it("should handle routes with trailing slashes in oldBasePath", () => {
expect(shiftBase("/base/foo", "/base/", "/new")).toBe("/new/foo");
expect(shiftBase("/base/foo/", "/base/", "/new")).toBe("/new/foo/");
expect(shiftBase("/base/", "/base/", "/new")).toBe("/new/");
});

it("should handle routes with mixed slashes and empty parts", () => {
expect(shiftBase("/base//foo", "/base", "/new")).toBe("/new/foo");
expect(shiftBase("/base/foo//", "/base", "/new")).toBe("/new/foo/");
expect(shiftBase("/base//", "/base", "/new")).toBe("/new/");
});

it("should handle newBaseUrl with https://", () => {
expect(shiftBase("/base/foo", "/base", "https://example.com/new")).toBe(
"https://example.com/new/foo",
);
expect(shiftBase("/base/foo/", "/base", "https://example.com/new")).toBe(
"https://example.com/new/foo/",
);
expect(shiftBase("/base/", "/base", "https://example.com/new")).toBe(
"https://example.com/new/",
);
expect(shiftBase("/base", "/base", "https://example.com/new")).toBe(
"https://example.com/new/",
);
});

it("should handle newBaseUrl with //example.com", () => {
expect(shiftBase("/base/foo", "/base", "//example.com/new")).toBe(
"//example.com/new/foo",
);
expect(shiftBase("/base/foo/", "/base", "//example.com/new")).toBe(
"//example.com/new/foo/",
);
expect(shiftBase("/base/", "/base", "//example.com/new")).toBe(
"//example.com/new/",
);
expect(shiftBase("/base", "/base", "//example.com/new")).toBe(
"//example.com/new/",
);
});

it("should handle newBaseUrl with only origin and no path", () => {
expect(shiftBase("/base/foo", "/base", "https://example.com")).toBe(
"https://example.com/foo",
);
expect(shiftBase("/base/foo/", "/base", "https://example.com")).toBe(
"https://example.com/foo/",
);
expect(shiftBase("/base/", "/base", "https://example.com")).toBe(
"https://example.com/",
);
expect(shiftBase("/base", "/base", "https://example.com")).toBe(
"https://example.com/",
);
});
});
});
19 changes: 19 additions & 0 deletions website/src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,22 @@ export const removeBasePath = (basePath: string, route: string): string => {
const offset = basePath.length - (basePath.endsWith("/") ? 1 : 0);
return route.slice(offset);
};

/**
* Replace the oldBasePath in a page route with a newBaseUrl.
*
* @param route - The route string to process.
* @param oldBasePath - The old base path to be removed from the route.
* @param newBaseUrl - The new base URL (may include origin) to be prepended to the route.
* @returns The route string with its base replaced.
*
* @example
* ```ts
* shiftBase("/base/foo/bar/", "/base/", "https://typst.app/docs/") // -> "https://typst.app/docs/foo/bar/"
* ```
*/
export const shiftBase = (
route: string,
oldBasePath: string,
newBaseUrl: string,
): string => joinPath(newBaseUrl, removeBasePath(oldBasePath, route));