Skip to content

Commit 8354fa6

Browse files
committed
Cleanup url parsing logic and tests
1 parent dc72251 commit 8354fa6

3 files changed

Lines changed: 77 additions & 231 deletions

File tree

app/modules/gh-docs/.server/doc-url-parser.test.ts

Lines changed: 25 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,23 @@ describe("parseDocUrl", () => {
1717
slug: "docs/start/modes",
1818
githubPath:
1919
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/start/modes.md",
20-
shouldRedirect: false,
20+
githubEditPath:
21+
"https://github.com/remix-run/react-router/edit/main/docs/start/modes.md",
2122
});
2223
});
2324

24-
it("should parse a nested doc page", () => {
25-
const url = new URL("https://reactrouter.com/tutorials/quick-start");
25+
it("should handle when URL ends with .md", () => {
26+
const url = new URL("https://reactrouter.com/getting-started.md");
2627
const splat = getSplat(url);
2728
const result = parseDocUrl(url, splat);
2829

2930
expect(result).toEqual({
3031
ref: "main",
31-
slug: "docs/tutorials/quick-start",
32+
slug: "docs/getting-started",
3233
githubPath:
33-
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/tutorials/quick-start.md",
34-
shouldRedirect: false,
34+
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/getting-started.md",
35+
githubEditPath:
36+
"https://github.com/remix-run/react-router/edit/main/docs/getting-started.md",
3537
});
3638
});
3739
});
@@ -47,7 +49,8 @@ describe("parseDocUrl", () => {
4749
slug: "docs/index",
4850
githubPath:
4951
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/index.md",
50-
shouldRedirect: false,
52+
githubEditPath:
53+
"https://github.com/remix-run/react-router/edit/main/docs/index.md",
5154
});
5255
});
5356

@@ -61,7 +64,8 @@ describe("parseDocUrl", () => {
6164
slug: "docs/index",
6265
githubPath:
6366
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/index.md",
64-
shouldRedirect: true,
67+
githubEditPath:
68+
"https://github.com/remix-run/react-router/edit/main/docs/index.md",
6569
});
6670
});
6771
});
@@ -77,7 +81,8 @@ describe("parseDocUrl", () => {
7781
slug: "CHANGELOG",
7882
githubPath:
7983
"https://raw.githubusercontent.com/remix-run/react-router/main/CHANGELOG.md",
80-
shouldRedirect: false,
84+
githubEditPath:
85+
"https://github.com/remix-run/react-router/edit/main/CHANGELOG.md",
8186
});
8287
});
8388

@@ -91,7 +96,8 @@ describe("parseDocUrl", () => {
9196
slug: "CHANGELOG",
9297
githubPath:
9398
"https://raw.githubusercontent.com/remix-run/react-router/main/CHANGELOG.md",
94-
shouldRedirect: true,
99+
githubEditPath:
100+
"https://github.com/remix-run/react-router/edit/main/CHANGELOG.md",
95101
});
96102
});
97103

@@ -105,7 +111,6 @@ describe("parseDocUrl", () => {
105111
slug: "CHANGELOG",
106112
githubPath:
107113
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@6.28.0/CHANGELOG.md",
108-
shouldRedirect: false,
109114
});
110115
});
111116
});
@@ -121,21 +126,8 @@ describe("parseDocUrl", () => {
121126
slug: "docs/start/modes",
122127
githubPath:
123128
"https://raw.githubusercontent.com/remix-run/react-router/dev/docs/start/modes.md",
124-
shouldRedirect: false,
125-
});
126-
});
127-
128-
it("should handle local ref", () => {
129-
const url = new URL("https://reactrouter.com/local/getting-started");
130-
const splat = getSplat(url);
131-
const result = parseDocUrl(url, splat);
132-
133-
expect(result).toEqual({
134-
ref: "local",
135-
slug: "docs/getting-started",
136-
githubPath:
137-
"https://raw.githubusercontent.com/remix-run/react-router/local/docs/getting-started.md",
138-
shouldRedirect: false,
129+
githubEditPath:
130+
"https://github.com/remix-run/react-router/edit/dev/docs/start/modes.md",
139131
});
140132
});
141133

@@ -149,12 +141,11 @@ describe("parseDocUrl", () => {
149141
slug: "docs/start/tutorial",
150142
githubPath:
151143
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@6.28.0/docs/start/tutorial.md",
152-
shouldRedirect: false,
153144
});
154145
});
155146

156-
it("should handle nested paths with version", () => {
157-
const url = new URL("https://reactrouter.com/6.28.0/start/tutorial");
147+
it("should handle semantic version ref with .md extension", () => {
148+
const url = new URL("https://reactrouter.com/6.28.0/start/tutorial.md");
158149
const splat = getSplat(url);
159150
const result = parseDocUrl(url, splat);
160151

@@ -163,160 +154,22 @@ describe("parseDocUrl", () => {
163154
slug: "docs/start/tutorial",
164155
githubPath:
165156
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@6.28.0/docs/start/tutorial.md",
166-
shouldRedirect: false,
167-
});
168-
});
169-
170-
it("should default to main for invalid version", () => {
171-
const url = new URL(
172-
"https://reactrouter.com/invalid-version/getting-started",
173-
);
174-
const splat = getSplat(url);
175-
const result = parseDocUrl(url, splat);
176-
177-
expect(result).toEqual({
178-
ref: "main",
179-
slug: "docs/invalid-version/getting-started",
180-
githubPath:
181-
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/invalid-version/getting-started.md",
182-
shouldRedirect: false,
183-
});
184-
});
185-
});
186-
187-
describe("markdown extension handling", () => {
188-
it("should flag for redirect when URL ends with .md", () => {
189-
const url = new URL("https://reactrouter.com/getting-started.md");
190-
const splat = getSplat(url);
191-
const result = parseDocUrl(url, splat);
192-
193-
expect(result.shouldRedirect).toBe(true);
194-
expect(result.slug).toBe("docs/getting-started.md");
195-
});
196-
197-
it("should handle .md extension with versioned URL", () => {
198-
const url = new URL("https://reactrouter.com/6.28.0/getting-started.md");
199-
const splat = getSplat(url);
200-
const result = parseDocUrl(url, splat);
201-
202-
expect(result).toEqual({
203-
ref: "6.28.0",
204-
slug: "docs/getting-started.md",
205-
githubPath:
206-
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@6.28.0/docs/getting-started.md",
207-
shouldRedirect: true,
208-
});
209-
});
210-
});
211-
212-
describe("edge cases", () => {
213-
it("should handle empty splat", () => {
214-
const url = new URL("https://reactrouter.com/");
215-
const splat = getSplat(url);
216-
const result = parseDocUrl(url, splat);
217-
218-
expect(result).toEqual({
219-
ref: "main",
220-
slug: "docs/",
221-
githubPath:
222-
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/.md",
223-
shouldRedirect: false,
224157
});
225158
});
226159

227-
it("should handle deep nested paths", () => {
160+
it("should handle pre-6.4.0 semantic version ref with v prefix", () => {
228161
const url = new URL(
229-
"https://reactrouter.com/guides/routing/lazy-loading",
162+
"https://reactrouter.com/6.2.0/getting-started/installation",
230163
);
231164
const splat = getSplat(url);
232165
const result = parseDocUrl(url, splat);
233166

234167
expect(result).toEqual({
235-
ref: "main",
236-
slug: "docs/guides/routing/lazy-loading",
168+
ref: "6.2.0",
169+
slug: "docs/getting-started/installation",
237170
githubPath:
238-
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/guides/routing/lazy-loading.md",
239-
shouldRedirect: false,
171+
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/v6.2.0/docs/getting-started/installation.md",
240172
});
241173
});
242-
243-
it("should handle paths with hyphens and numbers", () => {
244-
const url = new URL("https://reactrouter.com/v6.28.0/api-reference");
245-
const splat = getSplat(url);
246-
const result = parseDocUrl(url, splat);
247-
248-
expect(result).toEqual({
249-
ref: "v6.28.0",
250-
slug: "docs/api-reference",
251-
252-
githubPath:
253-
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@v6.28.0/docs/api-reference.md",
254-
shouldRedirect: false,
255-
});
256-
});
257-
});
258-
259-
describe("GitHub URL generation", () => {
260-
it("should use direct ref for main branch", () => {
261-
const url = new URL("https://reactrouter.com/getting-started");
262-
const splat = getSplat(url);
263-
const result = parseDocUrl(url, splat);
264-
265-
expect(result.githubPath).toBe(
266-
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/getting-started.md",
267-
);
268-
});
269-
270-
it("should use direct ref for dev branch", () => {
271-
const url = new URL("https://reactrouter.com/dev/getting-started");
272-
const splat = getSplat(url);
273-
const result = parseDocUrl(url, splat);
274-
275-
expect(result.githubPath).toBe(
276-
"https://raw.githubusercontent.com/remix-run/react-router/dev/docs/getting-started.md",
277-
);
278-
});
279-
280-
it("should use direct ref for local branch", () => {
281-
const url = new URL("https://reactrouter.com/local/getting-started");
282-
const splat = getSplat(url);
283-
const result = parseDocUrl(url, splat);
284-
285-
expect(result.githubPath).toBe(
286-
"https://raw.githubusercontent.com/remix-run/react-router/local/docs/getting-started.md",
287-
);
288-
});
289-
290-
it("should use refs/tags/ for semantic versions", () => {
291-
const url = new URL("https://reactrouter.com/v6.28.0/getting-started");
292-
const splat = getSplat(url);
293-
const result = parseDocUrl(url, splat);
294-
295-
expect(result.githubPath).toBe(
296-
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@v6.28.0/docs/getting-started.md",
297-
);
298-
});
299-
300-
it("should use refs/tags/ for semantic versions without v prefix", () => {
301-
const url = new URL("https://reactrouter.com/7.6.2/getting-started");
302-
const splat = getSplat(url);
303-
const result = parseDocUrl(url, splat);
304-
305-
expect(result.githubPath).toBe(
306-
"https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@7.6.2/docs/getting-started.md",
307-
);
308-
});
309-
310-
it("should use direct ref for invalid version strings", () => {
311-
const url = new URL(
312-
"https://reactrouter.com/invalid-version/getting-started",
313-
);
314-
const splat = getSplat(url);
315-
const result = parseDocUrl(url, splat);
316-
317-
expect(result.githubPath).toBe(
318-
"https://raw.githubusercontent.com/remix-run/react-router/main/docs/invalid-version/getting-started.md",
319-
);
320-
});
321174
});
322175
});
Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
11
import semver from "semver";
22

3-
function isRefBranch(ref: string): boolean {
4-
return ["dev", "main", "release-next", "local"].includes(ref);
3+
export function parseDocUrl(url: URL, splat: string) {
4+
// Remove the .md extension if there is one
5+
splat = splat.replace(/\.md$/, "");
6+
let pathname = url.pathname.replace(/\.md$/, "");
7+
8+
let firstSegment = splat.split("/")[0];
9+
10+
let ref = "main";
11+
if (
12+
firstSegment === "dev" ||
13+
firstSegment === "local" ||
14+
semver.valid(firstSegment)
15+
) {
16+
ref = firstSegment;
17+
}
18+
19+
let slug: string;
20+
if (pathname.endsWith("/changelog")) {
21+
slug = "CHANGELOG";
22+
} else if (pathname.endsWith("/home")) {
23+
slug = "docs/index";
24+
} else {
25+
// Build the docs path, removing refParam if present
26+
let docsPath = splat.replace(`${ref}/`, "");
27+
slug = `docs/${docsPath}`;
28+
}
29+
30+
return {
31+
ref,
32+
slug,
33+
...generateGitHubPaths(ref, slug),
34+
};
535
}
636

737
/**
@@ -24,66 +54,28 @@ export function fixupRefName(ref: string): string {
2454
return `react-router@${ref}`;
2555
}
2656

57+
function isRefBranch(ref: string): boolean {
58+
return ["dev", "main", "release-next", "local"].includes(ref);
59+
}
60+
2761
/**
2862
* Generates the correct GitHub raw URL based on the ref type
2963
* - For main/dev/local: uses the ref directly
3064
* - For semantic versions: uses refs/tags/{version}
3165
*/
32-
function generateGitHubRawUrl(ref: string, filePath: string): string {
66+
function generateGitHubPaths(ref: string, slug: string) {
3367
let baseUrl = "https://raw.githubusercontent.com/remix-run/react-router";
3468

3569
// For main, dev, local, or any non-semver ref, use directly
3670
if (isRefBranch(ref)) {
37-
return `${baseUrl}/${ref}/${filePath}`;
71+
return {
72+
githubPath: `${baseUrl}/${ref}/${slug}.md`,
73+
githubEditPath: `https://github.com/remix-run/react-router/edit/${ref}/${slug}.md`,
74+
};
3875
}
3976

4077
// For semantic versions, use refs/tags/ structure
41-
return `${baseUrl}/refs/tags/${fixupRefName(ref)}/${filePath}`;
42-
}
43-
44-
export function parseDocUrl(url: URL, splat: string) {
45-
let hasMdExtension = url.pathname.endsWith(".md");
46-
let firstSegment = splat.split("/")[0];
47-
let refParam =
48-
firstSegment === "dev" ||
49-
firstSegment === "local" ||
50-
semver.valid(firstSegment)
51-
? firstSegment
52-
: undefined;
53-
54-
let ref = refParam || "main";
55-
56-
let isHomePage =
57-
url.pathname.endsWith("/home") || url.pathname.endsWith("/home.md");
58-
let isChangelogPage =
59-
url.pathname.endsWith("/changelog") ||
60-
url.pathname.endsWith("/changelog.md");
61-
62-
let slug: string;
63-
if (isChangelogPage) {
64-
slug = "CHANGELOG";
65-
} else if (isHomePage) {
66-
slug = "docs/index";
67-
} else {
68-
// Build the docs path, removing refParam if present
69-
let docsPath = refParam ? splat.replace(`${refParam}/`, "") : splat;
70-
slug = `docs/${docsPath}`;
71-
}
72-
73-
let ghSlug: string;
74-
if (isChangelogPage || isHomePage) {
75-
ghSlug = `${slug}.md`;
76-
} else {
77-
// TODO: Figure out if we need this part
78-
ghSlug = hasMdExtension ? slug : `${slug}.md`;
79-
}
80-
81-
let githubPath = generateGitHubRawUrl(ref, ghSlug);
82-
8378
return {
84-
ref,
85-
slug,
86-
githubPath,
87-
shouldRedirect: hasMdExtension,
79+
githubPath: `${baseUrl}/refs/tags/${fixupRefName(ref)}/${slug}.md`,
8880
};
8981
}

0 commit comments

Comments
 (0)