@tomehq/theme 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/chunk-37JI6XGT.js +1720 -0
  3. package/dist/chunk-3A2LPGUL.js +1991 -0
  4. package/dist/chunk-3I2QTWTW.js +1948 -0
  5. package/dist/chunk-45M5UIAB.js +2110 -0
  6. package/dist/chunk-462AGU3S.js +1959 -0
  7. package/dist/chunk-7MUTU5D4.js +1720 -0
  8. package/dist/chunk-BZGWSKT2.js +573 -0
  9. package/dist/chunk-CTPOZMMK.js +1703 -0
  10. package/dist/chunk-DO544M3G.js +1702 -0
  11. package/dist/chunk-DPKZBFQP.js +1777 -0
  12. package/dist/chunk-FWBTK5TL.js +1444 -0
  13. package/dist/chunk-GDQIBNX5.js +1962 -0
  14. package/dist/chunk-INUMUXN5.js +2095 -0
  15. package/dist/chunk-JA4PMX6M.js +1500 -0
  16. package/dist/chunk-JZRT4WNC.js +1441 -0
  17. package/dist/chunk-LIMYFTPC.js +1468 -0
  18. package/dist/chunk-MEP7P6A7.js +1500 -0
  19. package/dist/chunk-NOZBIES7.js +1948 -0
  20. package/dist/chunk-O4GH3KYX.js +1712 -0
  21. package/dist/chunk-OEXM3BEC.js +1702 -0
  22. package/dist/chunk-Q7PYTVW3.js +1771 -0
  23. package/dist/chunk-QCWZYABW.js +1468 -0
  24. package/dist/chunk-RDF25WB2.js +2085 -0
  25. package/dist/chunk-RKTT3ZEX.js +1500 -0
  26. package/dist/chunk-S47BRMNQ.js +1715 -0
  27. package/dist/chunk-S4ZH5F56.js +1949 -0
  28. package/dist/chunk-SRD7NJHS.js +1949 -0
  29. package/dist/chunk-TQDWPSTO.js +2087 -0
  30. package/dist/chunk-TTRXRPP6.js +1941 -0
  31. package/dist/chunk-UKYFJSUA.js +509 -0
  32. package/dist/chunk-VUT2FMSI.js +1937 -0
  33. package/dist/chunk-VVCC5JHK.js +1949 -0
  34. package/dist/chunk-W732TVBK.js +1944 -0
  35. package/dist/chunk-X4VQYPKO.js +1768 -0
  36. package/dist/entry.js +1 -1
  37. package/dist/index.d.ts +7 -1
  38. package/dist/index.js +1 -1
  39. package/package.json +3 -3
  40. package/src/Shell.test.tsx +72 -0
  41. package/src/Shell.tsx +204 -16
  42. package/src/entry-helpers.test.ts +316 -0
  43. package/src/entry-helpers.ts +103 -0
  44. package/src/entry.tsx +160 -69
  45. package/src/routing.test.ts +124 -0
  46. package/src/routing.ts +45 -0
package/src/entry.tsx CHANGED
@@ -1,11 +1,19 @@
1
1
  import React, { useState, useEffect, useCallback } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { Shell } from "./Shell.js";
4
+ import { pathnameToPageId as _pathnameToPageId, pageIdToPath as _pageIdToPath } from "./routing.js";
5
+ import {
6
+ loadPage,
7
+ computeEditUrl,
8
+ resolveInitialPageId,
9
+ detectCurrentVersion,
10
+ type LoadedPage,
11
+ } from "./entry-helpers.js";
4
12
 
5
13
  // @ts-ignore — resolved by vite-plugin-tome
6
14
  import config from "virtual:tome/config";
7
15
  // @ts-ignore — resolved by vite-plugin-tome
8
- import { routes, navigation } from "virtual:tome/routes";
16
+ import { routes, navigation, versions } from "virtual:tome/routes";
9
17
  // @ts-ignore — resolved by vite-plugin-tome
10
18
  import loadPageModule from "virtual:tome/page-loader";
11
19
  // @ts-ignore — resolved by vite-plugin-tome
@@ -21,6 +29,9 @@ import {
21
29
  Steps,
22
30
  Accordion,
23
31
  ChangelogTimeline,
32
+ PackageManager,
33
+ TypeTable,
34
+ FileTree,
24
35
  } from "@tomehq/components";
25
36
 
26
37
  const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
@@ -31,6 +42,9 @@ const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
31
42
  Steps,
32
43
  Accordion,
33
44
  ChangelogTimeline,
45
+ PackageManager,
46
+ TypeTable,
47
+ FileTree, // Sub-components accessible as <FileTree.File /> and <FileTree.Folder /> in MDX
34
48
  };
35
49
 
36
50
  // ── CONTENT STYLES ───────────────────────────────────────
@@ -56,8 +70,11 @@ const contentStyles = `
56
70
  .tome-content table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }
57
71
  .tome-content th, .tome-content td { padding: 0.5em 0.8em; border: 1px solid var(--bd); text-align: left; font-size: 0.9em; }
58
72
  .tome-content th { background: var(--sf); font-weight: 600; }
59
- .tome-content img { max-width: 100%; border-radius: 2px; }
73
+ .tome-content img { max-width: 100%; border-radius: 2px; cursor: zoom-in; }
60
74
  .tome-content hr { border: none; border-top: 1px solid var(--bd); margin: 2em 0; }
75
+ .tome-mermaid { margin: 1.2em 0; text-align: center; overflow-x: auto; }
76
+ .tome-mermaid svg { max-width: 100%; height: auto; overflow: visible; }
77
+ .tome-mermaid svg .nodeLabel { overflow: visible; white-space: nowrap; }
61
78
 
62
79
  /* Mobile responsive content */
63
80
  @media (max-width: 767px) {
@@ -97,105 +114,176 @@ const contentStyles = `
97
114
  html.dark .shiki span[style*="--shiki-dark:#6A737D"] {
98
115
  --shiki-dark: #a0aab5 !important;
99
116
  }
117
+
118
+ /* Light mode: darken low-contrast github-light tokens for WCAG AA on --cdBg backgrounds */
119
+ html:not(.dark) .shiki span[style*="color:#6A737D"] { color: #57606a !important; }
120
+ html:not(.dark) .shiki span[style*="color:#E36209"] { color: #b35405 !important; }
121
+ html:not(.dark) .shiki span[style*="color:#6F42C1"] { color: #5a32a3 !important; }
122
+ html:not(.dark) .shiki span[style*="color:#22863A"] { color: #1a6e2e !important; }
123
+ html:not(.dark) .shiki span[style*="color:#D73A49"] { color: #b62324 !important; }
124
+ html:not(.dark) .shiki span[style*="color:#005CC5"] { color: #0349b4 !important; }
100
125
  `;
101
126
 
102
- // ── PAGE TYPES ────────────────────────────────────────────
103
- interface HtmlPage {
104
- isMdx: false;
105
- html: string;
106
- frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
107
- headings: Array<{ depth: number; text: string; id: string }>;
108
- changelogEntries?: Array<{ version: string; date?: string; url?: string; sections: Array<{ type: string; items: string[] }> }>;
109
- }
127
+ // ── ROUTING HELPERS ──────────────────────────────────────
128
+ const basePath = (config.basePath || "/").replace(/\/$/, "");
110
129
 
111
- interface MdxPage {
112
- isMdx: true;
113
- component: React.ComponentType<{ components?: Record<string, React.ComponentType> }>;
114
- frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
115
- headings: Array<{ depth: number; text: string; id: string }>;
130
+ function pathnameToPageId(pathname: string): string | null {
131
+ return _pathnameToPageId(pathname, basePath, routes);
116
132
  }
117
133
 
118
- type LoadedPage = HtmlPage | MdxPage;
119
-
120
- // ── PAGE LOADER ──────────────────────────────────────────
121
- async function loadPage(id: string): Promise<LoadedPage | null> {
122
- try {
123
- const route = routes.find((r: any) => r.id === id);
124
- const mod = await loadPageModule(id);
125
-
126
- if (route?.isMdx && mod.meta) {
127
- // TOM-8: MDX page — mod.default is the React component
128
- return {
129
- isMdx: true,
130
- component: mod.default,
131
- frontmatter: mod.meta.frontmatter,
132
- headings: mod.meta.headings,
133
- };
134
- }
135
-
136
- // Regular .md page — mod.default is { html, frontmatter, headings }
137
- if (!mod.default) return null;
138
-
139
- // TOM-49: Changelog page type
140
- if (mod.isChangelog && mod.changelogEntries) {
141
- return { isMdx: false, ...mod.default, changelogEntries: mod.changelogEntries };
142
- }
143
-
144
- return { isMdx: false, ...mod.default };
145
- } catch (err) {
146
- console.error(`Failed to load page: ${id}`, err);
147
- return null;
148
- }
134
+ function pageIdToPath(id: string): string {
135
+ return _pageIdToPath(id, basePath, routes);
149
136
  }
150
137
 
151
138
  // ── APP ──────────────────────────────────────────────────
152
139
  function App() {
153
- const [currentPageId, setCurrentPageId] = useState(() => {
154
- const hash = window.location.hash.slice(1);
155
- if (hash && routes.some((r: any) => r.id === hash)) return hash;
156
- return routes[0]?.id || "index";
157
- });
140
+ const [currentPageId, setCurrentPageId] = useState(() =>
141
+ resolveInitialPageId(
142
+ window.location.pathname,
143
+ window.location.hash,
144
+ routes,
145
+ basePath,
146
+ _pathnameToPageId,
147
+ )
148
+ );
158
149
 
159
150
  const [pageData, setPageData] = useState<LoadedPage | null>(null);
160
151
  const [loading, setLoading] = useState(true);
161
152
 
162
- const navigateTo = useCallback(async (id: string) => {
153
+ const navigateTo = useCallback(async (id: string, opts?: { replace?: boolean; skipScroll?: boolean }) => {
163
154
  setLoading(true);
164
155
  setCurrentPageId(id);
165
- window.location.hash = id;
166
- const data = await loadPage(id);
156
+ const fullPath = pageIdToPath(id);
157
+ if (opts?.replace) {
158
+ window.history.replaceState(null, "", fullPath);
159
+ } else {
160
+ window.history.pushState(null, "", fullPath);
161
+ }
162
+ const data = await loadPage(id, routes, loadPageModule);
167
163
  setPageData(data);
168
164
  setLoading(false);
165
+ // Scroll to heading anchor if present, otherwise scroll to top
166
+ if (!opts?.skipScroll) {
167
+ const anchor = window.location.hash.slice(1);
168
+ if (anchor) {
169
+ requestAnimationFrame(() => {
170
+ const el = document.getElementById(anchor);
171
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
172
+ });
173
+ } else {
174
+ window.scrollTo(0, 0);
175
+ }
176
+ }
169
177
  }, []);
170
178
 
171
- useEffect(() => { navigateTo(currentPageId); }, []);
179
+ // Initial page load
180
+ useEffect(() => {
181
+ // If user landed on a legacy hash URL, redirect to clean path
182
+ const hash = window.location.hash.slice(1);
183
+ if (hash && routes.some((r: any) => r.id === hash)) {
184
+ const fullPath = pageIdToPath(hash);
185
+ window.history.replaceState(null, "", fullPath);
186
+ navigateTo(hash, { replace: true });
187
+ } else {
188
+ navigateTo(currentPageId, { replace: true, skipScroll: true });
189
+ }
190
+ }, []);
172
191
 
192
+ // Listen for browser back/forward navigation
173
193
  useEffect(() => {
174
- const onHashChange = () => {
175
- const hash = window.location.hash.slice(1);
176
- // Only navigate if hash matches a known route ID (ignore heading anchors)
177
- if (hash && hash !== currentPageId && routes.some((r: any) => r.id === hash)) {
178
- navigateTo(hash);
194
+ const onPopState = () => {
195
+ const id = pathnameToPageId(window.location.pathname);
196
+ if (id && id !== currentPageId) {
197
+ navigateTo(id, { replace: true, skipScroll: true });
179
198
  }
180
199
  };
181
- window.addEventListener("hashchange", onHashChange);
182
- return () => window.removeEventListener("hashchange", onHashChange);
200
+ window.addEventListener("popstate", onPopState);
201
+ return () => window.removeEventListener("popstate", onPopState);
183
202
  }, [currentPageId, navigateTo]);
184
203
 
204
+ // Mermaid diagram rendering: load from CDN and render .tome-mermaid elements
205
+ useEffect(() => {
206
+ const els = document.querySelectorAll(".tome-mermaid[data-mermaid]");
207
+ if (els.length === 0) return;
208
+ let cancelled = false;
209
+
210
+ const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
211
+
212
+ (async () => {
213
+ try {
214
+ // Load mermaid from CDN (ESM) — works in all browsers, no bundler dependency
215
+ const { default: mermaid } = await import(/* @vite-ignore */ MERMAID_CDN);
216
+ if (cancelled) return;
217
+ const isDark = document.documentElement.classList.contains("dark");
218
+ // Resolve CSS variable to concrete font name — mermaid can't resolve CSS vars for text measurement
219
+ const resolvedFont = getComputedStyle(document.documentElement).getPropertyValue("--font-body").trim() || "sans-serif";
220
+ mermaid.initialize({
221
+ startOnLoad: false,
222
+ theme: isDark ? "dark" : "default",
223
+ fontFamily: resolvedFont,
224
+ flowchart: { padding: 15, nodeSpacing: 30, rankSpacing: 40 },
225
+ });
226
+
227
+ for (let i = 0; i < els.length; i++) {
228
+ const el = els[i] as HTMLElement;
229
+ if (el.querySelector("svg")) continue; // already rendered
230
+ const encoded = el.getAttribute("data-mermaid");
231
+ if (!encoded) continue;
232
+ try {
233
+ const code = atob(encoded);
234
+ const { svg } = await mermaid.render(`tome-mermaid-${i}-${Date.now()}`, code);
235
+ if (!cancelled) {
236
+ // Sanitize SVG to prevent XSS from mermaid-rendered content
237
+ try {
238
+ // @ts-ignore — CDN dynamic import for browser-only sanitization
239
+ const DOMPurify = (await import(/* @vite-ignore */ "https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.es.mjs")).default;
240
+ el.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } });
241
+ } catch {
242
+ // DOMPurify unavailable — render without sanitization (acceptable for trusted content)
243
+ el.innerHTML = svg;
244
+ }
245
+ }
246
+ } catch (err) {
247
+ console.warn("[tome] Mermaid render failed:", err);
248
+ el.textContent = "Diagram rendering failed";
249
+ (el as HTMLElement).style.cssText = "padding:16px;color:var(--txM);font-size:13px;border:1px dashed var(--bd);border-radius:2px;text-align:center;";
250
+ }
251
+ }
252
+ } catch (err) {
253
+ console.warn("[tome] Failed to load mermaid from CDN:", err);
254
+ els.forEach((el) => {
255
+ (el as HTMLElement).textContent = "Failed to load diagram renderer";
256
+ (el as HTMLElement).style.cssText = "padding:16px;color:var(--txM);font-size:13px;border:1px dashed var(--bd);border-radius:2px;text-align:center;";
257
+ });
258
+ }
259
+ })();
260
+
261
+ return () => { cancelled = true; };
262
+ }, [pageData, loading]);
263
+
185
264
  const allPages = routes.map((r: any) => ({
186
265
  id: r.id,
187
266
  title: r.frontmatter.title,
188
267
  description: r.frontmatter.description,
189
268
  }));
190
269
 
191
- // TOM-48: Compute edit URL for current page
270
+ // Compute current version from route metadata
192
271
  const currentRoute = routes.find((r: any) => r.id === currentPageId);
193
- let editUrl: string | undefined;
194
- if (config.editLink && currentRoute?.filePath) {
195
- const { repo, branch = "main", dir = "" } = config.editLink;
196
- const dirPrefix = dir ? `${dir.replace(/\/$/, "")}/` : "";
197
- editUrl = `https://github.com/${repo}/edit/${branch}/${dirPrefix}${currentRoute.filePath}`;
198
- }
272
+ const currentVersion = detectCurrentVersion(currentRoute, versions);
273
+ const editUrl = computeEditUrl(config.editLink, currentRoute?.filePath);
274
+
275
+ // KaTeX CSS: inject stylesheet when math is enabled
276
+ useEffect(() => {
277
+ if (!(config as any).math) return;
278
+ const id = "tome-katex-css";
279
+ if (document.getElementById(id)) return;
280
+ const link = document.createElement("link");
281
+ link.id = id;
282
+ link.rel = "stylesheet";
283
+ link.href = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
284
+ link.crossOrigin = "anonymous";
285
+ document.head.appendChild(link);
286
+ }, []);
199
287
 
200
288
  return (
201
289
  <>
@@ -217,6 +305,9 @@ function App() {
217
305
  onNavigate={navigateTo}
218
306
  allPages={allPages}
219
307
  docContext={docContext}
308
+ versioning={versions || undefined}
309
+ currentVersion={currentVersion}
310
+ basePath={basePath}
220
311
  />
221
312
  </>
222
313
  );
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { pathnameToPageId, pageIdToPath } from "./routing.js";
3
+ import type { MinimalRoute } from "./routing.js";
4
+
5
+ // ── Shared fixtures ──────────────────────────────────────
6
+
7
+ const routes: MinimalRoute[] = [
8
+ { id: "index", urlPath: "/" },
9
+ { id: "quickstart", urlPath: "/quickstart" },
10
+ { id: "installation", urlPath: "/installation" },
11
+ { id: "reference/config", urlPath: "/reference/config" },
12
+ { id: "guides/migration", urlPath: "/guides/migration" },
13
+ { id: "v1/index", urlPath: "/v1/" },
14
+ { id: "v1/quickstart", urlPath: "/v1/quickstart" },
15
+ ];
16
+
17
+ // ── pathnameToPageId ─────────────────────────────────────
18
+
19
+ describe("pathnameToPageId", () => {
20
+ const basePath = "/docs";
21
+
22
+ it("resolves root path to index", () => {
23
+ expect(pathnameToPageId("/docs/", basePath, routes)).toBe("index");
24
+ });
25
+
26
+ it("resolves root without trailing slash", () => {
27
+ expect(pathnameToPageId("/docs", basePath, routes)).toBe("index");
28
+ });
29
+
30
+ it("resolves top-level page", () => {
31
+ expect(pathnameToPageId("/docs/quickstart", basePath, routes)).toBe("quickstart");
32
+ });
33
+
34
+ it("resolves nested page", () => {
35
+ expect(pathnameToPageId("/docs/reference/config", basePath, routes)).toBe("reference/config");
36
+ });
37
+
38
+ it("resolves deeply nested page", () => {
39
+ expect(pathnameToPageId("/docs/guides/migration", basePath, routes)).toBe("guides/migration");
40
+ });
41
+
42
+ it("returns null for unknown page", () => {
43
+ expect(pathnameToPageId("/docs/nonexistent", basePath, routes)).toBeNull();
44
+ });
45
+
46
+ it("strips /index.html suffix", () => {
47
+ expect(pathnameToPageId("/docs/quickstart/index.html", basePath, routes)).toBe("quickstart");
48
+ });
49
+
50
+ it("strips .html suffix", () => {
51
+ expect(pathnameToPageId("/docs/quickstart.html", basePath, routes)).toBe("quickstart");
52
+ });
53
+
54
+ it("strips trailing slash", () => {
55
+ expect(pathnameToPageId("/docs/quickstart/", basePath, routes)).toBe("quickstart");
56
+ });
57
+
58
+ it("resolves versioned page", () => {
59
+ expect(pathnameToPageId("/docs/v1/quickstart", basePath, routes)).toBe("v1/quickstart");
60
+ });
61
+
62
+ it("resolves versioned index", () => {
63
+ // /docs/v1/ → strip basePath → /v1/ → strip leading / → v1/ → strip trailing / → v1
64
+ // But our route ID is "v1/index", not "v1", so this needs to resolve properly
65
+ // After all stripping: "v1" — not in routes (v1/index is). Let's test this edge case.
66
+ expect(pathnameToPageId("/docs/v1", basePath, routes)).toBeNull();
67
+ });
68
+
69
+ describe("with empty basePath", () => {
70
+ it("resolves root to index", () => {
71
+ expect(pathnameToPageId("/", "", routes)).toBe("index");
72
+ });
73
+
74
+ it("resolves page at root", () => {
75
+ expect(pathnameToPageId("/quickstart", "", routes)).toBe("quickstart");
76
+ });
77
+
78
+ it("resolves nested page at root", () => {
79
+ expect(pathnameToPageId("/reference/config", "", routes)).toBe("reference/config");
80
+ });
81
+ });
82
+
83
+ describe("with / basePath", () => {
84
+ it("resolves top-level page", () => {
85
+ expect(pathnameToPageId("/quickstart", "", routes)).toBe("quickstart");
86
+ });
87
+ });
88
+ });
89
+
90
+ // ── pageIdToPath ─────────────────────────────────────────
91
+
92
+ describe("pageIdToPath", () => {
93
+ const basePath = "/docs";
94
+
95
+ it("builds path for index", () => {
96
+ expect(pageIdToPath("index", basePath, routes)).toBe("/docs/");
97
+ });
98
+
99
+ it("builds path for top-level page", () => {
100
+ expect(pageIdToPath("quickstart", basePath, routes)).toBe("/docs/quickstart");
101
+ });
102
+
103
+ it("builds path for nested page", () => {
104
+ expect(pageIdToPath("reference/config", basePath, routes)).toBe("/docs/reference/config");
105
+ });
106
+
107
+ it("builds path for versioned page", () => {
108
+ expect(pageIdToPath("v1/quickstart", basePath, routes)).toBe("/docs/v1/quickstart");
109
+ });
110
+
111
+ it("falls back to basePath + id for unknown route", () => {
112
+ expect(pageIdToPath("unknown-page", basePath, routes)).toBe("/docs/unknown-page");
113
+ });
114
+
115
+ describe("with empty basePath", () => {
116
+ it("builds path for index", () => {
117
+ expect(pageIdToPath("index", "", routes)).toBe("/");
118
+ });
119
+
120
+ it("builds path for page", () => {
121
+ expect(pageIdToPath("quickstart", "", routes)).toBe("/quickstart");
122
+ });
123
+ });
124
+ });
package/src/routing.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pure routing utilities for History API navigation.
3
+ * Extracted from entry.tsx so they can be unit-tested independently.
4
+ */
5
+
6
+ export interface MinimalRoute {
7
+ id: string;
8
+ urlPath: string;
9
+ }
10
+
11
+ /**
12
+ * Extract page ID from a pathname by stripping basePath prefix.
13
+ * Returns null if the resolved ID doesn't match any known route.
14
+ */
15
+ export function pathnameToPageId(
16
+ pathname: string,
17
+ basePath: string,
18
+ routes: MinimalRoute[],
19
+ ): string | null {
20
+ let relative = pathname;
21
+ if (basePath && relative.startsWith(basePath)) {
22
+ relative = relative.slice(basePath.length);
23
+ }
24
+ const id =
25
+ relative
26
+ .replace(/^\//, "")
27
+ .replace(/\/index\.html$/, "")
28
+ .replace(/\.html$/, "")
29
+ .replace(/\/$/, "") || "index";
30
+ const route = routes.find((r) => r.id === id);
31
+ return route ? id : null;
32
+ }
33
+
34
+ /**
35
+ * Get the full URL path for a page ID (basePath + urlPath).
36
+ */
37
+ export function pageIdToPath(
38
+ id: string,
39
+ basePath: string,
40
+ routes: MinimalRoute[],
41
+ ): string {
42
+ const route = routes.find((r) => r.id === id);
43
+ if (route) return basePath + route.urlPath;
44
+ return basePath + "/" + id;
45
+ }