starlight-telescope 0.0.1

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.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * URL utilities for building locale-aware URLs.
3
+ * Based on the starlight-tags plugin's approach.
4
+ * @see https://github.com/HiDeoo/starlight-blog/blob/main/packages/starlight-blog/libs/page.ts
5
+ */
6
+
7
+ // Lazy-loaded config to avoid circular dependencies
8
+ let starlightLocales: Record<string, unknown> | undefined;
9
+
10
+ function getLocales(): Record<string, unknown> | undefined {
11
+ if (starlightLocales === undefined) {
12
+ try {
13
+ // @ts-expect-error - virtual module
14
+ const config = import.meta.env.STARLIGHT_LOCALES;
15
+ starlightLocales = config ? JSON.parse(config) : undefined;
16
+ } catch {
17
+ starlightLocales = undefined;
18
+ }
19
+ }
20
+ return starlightLocales;
21
+ }
22
+
23
+ /**
24
+ * Get the base URL with trailing slash removed.
25
+ */
26
+ export function getBase(): string {
27
+ return import.meta.env.BASE_URL.replace(/\/$/, '');
28
+ }
29
+
30
+ /**
31
+ * Extract locale from a URL path's first segment.
32
+ * Returns undefined if no locale found (root locale).
33
+ */
34
+ export function getLocaleFromPath(path: string): string | undefined {
35
+ // Remove leading slash and get first segment
36
+ const segments = path.replace(/^\//, '').split('/');
37
+ const firstSegment = segments[0];
38
+
39
+ if (!firstSegment) return undefined;
40
+
41
+ // Check against configured locales if available
42
+ const locales = getLocales();
43
+ if (locales && firstSegment in locales) {
44
+ return firstSegment;
45
+ }
46
+
47
+ // Fallback: check if it looks like a locale code (e.g., 'en', 'fr', 'en-us', 'pt-br')
48
+ // This handles cases where we can't access the config
49
+ //
50
+ // We use a stricter pattern to avoid false positives with common path segments.
51
+ // Valid patterns:
52
+ // - Exactly 2 lowercase letters (ISO 639-1): en, fr, de, es, ja, zh
53
+ // - 2 letters + hyphen + 2 letters/digits (regional): en-us, pt-br, zh-cn
54
+ // - 2 letters + hyphen + 4 letters (script variants): zh-hans, zh-hant
55
+ //
56
+ // Explicitly excluded common false positives: api, src, css, img, lib, etc.
57
+ const commonPathSegments = new Set([
58
+ 'api', 'src', 'css', 'img', 'lib', 'app', 'bin', 'doc', 'log', 'tmp', 'var', 'opt', 'usr', 'etc'
59
+ ]);
60
+
61
+ if (commonPathSegments.has(firstSegment)) {
62
+ return undefined;
63
+ }
64
+
65
+ // Match strict locale patterns: xx or xx-xx or xx-xxxx
66
+ if (/^[a-z]{2}(-[a-z]{2,4})?$/.test(firstSegment)) {
67
+ return firstSegment;
68
+ }
69
+
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Extract locale from an Astro URL, accounting for base path.
75
+ */
76
+ export function getLocaleFromUrl(url: URL): string | undefined {
77
+ const base = getBase();
78
+ const pathAfterBase = url.pathname.slice(base.length);
79
+ return getLocaleFromPath(pathAfterBase);
80
+ }
81
+
82
+ /**
83
+ * Build a localized URL from a relative path.
84
+ *
85
+ * @param path - Relative path (e.g., 'getting-started')
86
+ * @param locale - Locale string or undefined for root locale
87
+ * @returns Full URL with base and locale (e.g., '/docs/fr/getting-started')
88
+ */
89
+ export function buildUrl(path: string, locale: string | undefined): string {
90
+ const base = getBase();
91
+ const localeSegment = locale ? `/${locale}` : '';
92
+
93
+ // Ensure path starts with /
94
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
95
+
96
+ return `${base}${localeSegment}${normalizedPath}`;
97
+ }
98
+
99
+ /**
100
+ * Build a localized URL, extracting locale from the current page URL.
101
+ * Use this when you don't have access to Astro.params.
102
+ *
103
+ * @param path - Relative path (e.g., 'getting-started')
104
+ * @param currentUrl - The current page's URL (Astro.url)
105
+ * @returns Full URL with base and locale
106
+ */
107
+ export function buildUrlFromCurrentPage(path: string, currentUrl: URL): string {
108
+ const locale = getLocaleFromUrl(currentUrl);
109
+ return buildUrl(path, locale);
110
+ }
111
+
112
+ /**
113
+ * Strip locale prefix from a path if present.
114
+ *
115
+ * @param path - Path that may include locale prefix (e.g., 'fr/getting-started')
116
+ * @returns Path without locale prefix (e.g., 'getting-started')
117
+ */
118
+ export function stripLocaleFromPath(path: string): string {
119
+ const locale = getLocaleFromPath(path);
120
+ if (locale) {
121
+ // Remove locale and the following slash
122
+ return path.slice(locale.length + 1);
123
+ }
124
+ return path;
125
+ }
@@ -0,0 +1,19 @@
1
+ import type { Plugin as VitePlugin } from 'vite';
2
+ import type { TelescopeConfig } from '../schemas/config.js';
3
+
4
+ export function vitePluginTelescopeConfig(config: TelescopeConfig): VitePlugin {
5
+ const VIRTUAL_ID = 'virtual:starlight-telescope-config';
6
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
7
+
8
+ return {
9
+ name: 'vite-plugin-starlight-telescope',
10
+ resolveId(id) {
11
+ if (id === VIRTUAL_ID) return RESOLVED_ID;
12
+ },
13
+ load(id) {
14
+ if (id === RESOLVED_ID) {
15
+ return `export default ${JSON.stringify(config)};`;
16
+ }
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,72 @@
1
+ import type { APIRoute, GetStaticPaths } from 'astro';
2
+ import { getCollection } from 'astro:content';
3
+ // @ts-expect-error - virtual module provided by Starlight
4
+ import starlightConfig from 'virtual:starlight/user-config';
5
+ import type { TelescopePage } from '../schemas/config.js';
6
+ import { getLocaleFromPath, stripLocaleFromPath } from '../libs/url.js';
7
+
8
+ /**
9
+ * Generate static paths for all supported locales.
10
+ * Creates /pages.json for root locale and /[locale]/pages.json for each configured locale.
11
+ */
12
+ export const getStaticPaths: GetStaticPaths = () => {
13
+ const configuredLocales = starlightConfig.locales
14
+ ? Object.keys(starlightConfig.locales).filter((l) => l !== 'root')
15
+ : [];
16
+
17
+ return [
18
+ // Root locale (no prefix)
19
+ { params: { locale: undefined } },
20
+ // Each configured locale
21
+ ...configuredLocales.map((locale) => ({
22
+ params: { locale },
23
+ })),
24
+ ];
25
+ };
26
+
27
+ export const GET: APIRoute = async ({ params }) => {
28
+ const { locale } = params;
29
+
30
+ try {
31
+ const allPages = await getCollection('docs');
32
+
33
+ // Filter pages by locale
34
+ const localePages = allPages.filter((page) => {
35
+ const pageLocale = getLocaleFromPath(page.id);
36
+
37
+ if (locale === undefined) {
38
+ // Root locale: pages without locale prefix
39
+ return pageLocale === undefined;
40
+ }
41
+ // Specific locale: pages with matching locale prefix
42
+ return pageLocale === locale;
43
+ });
44
+
45
+ // Filter out the home page (served at /, not /index)
46
+ const filteredPages = localePages.filter(
47
+ (page) => stripLocaleFromPath(page.id) !== 'index'
48
+ );
49
+
50
+ // Format pages, stripping locale from path since it's already implicit
51
+ const formattedPages: TelescopePage[] = filteredPages.map((page) => ({
52
+ title: page.data.title,
53
+ // Strip locale prefix from path - navigation will add it back
54
+ path: stripLocaleFromPath(page.id),
55
+ description: page.data.description || '',
56
+ tags: page.data.tags || [],
57
+ }));
58
+
59
+ return new Response(JSON.stringify(formattedPages), {
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'Cache-Control': 'max-age=3600', // Cache for 1 hour
63
+ },
64
+ });
65
+ } catch (error) {
66
+ console.error('Error generating pages.json:', error);
67
+ return new Response(JSON.stringify({ error: 'Failed to generate pages' }), {
68
+ status: 500,
69
+ headers: { 'Content-Type': 'application/json' },
70
+ });
71
+ }
72
+ };
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+
3
+ // Keyboard shortcut schema
4
+ export const ShortcutSchema = z
5
+ .object({
6
+ key: z.string().default('/'),
7
+ ctrl: z.boolean().default(true),
8
+ meta: z.boolean().default(true), // Cmd on Mac
9
+ shift: z.boolean().default(false),
10
+ alt: z.boolean().default(false),
11
+ })
12
+ .default({
13
+ key: '/',
14
+ ctrl: true,
15
+ meta: true,
16
+ shift: false,
17
+ alt: false,
18
+ });
19
+
20
+ // Fuse.js options schema - optimized for quick page navigation
21
+ export const FuseOptionsSchema = z
22
+ .object({
23
+ threshold: z.number().min(0).max(1).default(0.3),
24
+ ignoreLocation: z.boolean().default(true),
25
+ distance: z.number().default(100),
26
+ minMatchCharLength: z.number().default(2),
27
+ findAllMatches: z.boolean().default(false),
28
+ keys: z
29
+ .array(
30
+ z.object({
31
+ name: z.string(),
32
+ weight: z.number(),
33
+ })
34
+ )
35
+ .default([
36
+ { name: 'title', weight: 1.0 },
37
+ { name: 'path', weight: 0.6 },
38
+ { name: 'tags', weight: 0.5 },
39
+ { name: 'description', weight: 0.3 },
40
+ ]),
41
+ })
42
+ .default({
43
+ threshold: 0.3,
44
+ ignoreLocation: true,
45
+ distance: 100,
46
+ minMatchCharLength: 2,
47
+ findAllMatches: false,
48
+ keys: [
49
+ { name: 'title', weight: 1.0 },
50
+ { name: 'path', weight: 0.6 },
51
+ { name: 'tags', weight: 0.5 },
52
+ { name: 'description', weight: 0.3 },
53
+ ],
54
+ });
55
+
56
+ // Theme customization schema
57
+ export const ThemeSchema = z
58
+ .object({
59
+ overlayBackground: z.string().optional(),
60
+ modalBackground: z.string().optional(),
61
+ modalBackgroundAlt: z.string().optional(),
62
+ accentColor: z.string().optional(),
63
+ accentHover: z.string().optional(),
64
+ accentSelected: z.string().optional(),
65
+ textPrimary: z.string().optional(),
66
+ textSecondary: z.string().optional(),
67
+ border: z.string().optional(),
68
+ borderActive: z.string().optional(),
69
+ pinColor: z.string().optional(),
70
+ tagColor: z.string().optional(),
71
+ })
72
+ .default({});
73
+
74
+ // Main config schema
75
+ export const TelescopeConfigSchema = z
76
+ .object({
77
+ shortcut: ShortcutSchema,
78
+ fuseOptions: FuseOptionsSchema,
79
+ recentPagesCount: z.number().min(0).max(20).default(5),
80
+ maxResults: z.number().min(1).max(100).default(20),
81
+ debounceMs: z.number().min(0).max(1000).default(100),
82
+ theme: ThemeSchema,
83
+ })
84
+ .default({
85
+ recentPagesCount: 5,
86
+ maxResults: 20,
87
+ debounceMs: 100,
88
+ });
89
+
90
+ // Inferred types
91
+ export type TelescopeConfig = z.infer<typeof TelescopeConfigSchema>;
92
+ export type TelescopeUserConfig = z.input<typeof TelescopeConfigSchema>;
93
+ export type TelescopeShortcut = z.infer<typeof ShortcutSchema>;
94
+ export type TelescopeFuseOptions = z.infer<typeof FuseOptionsSchema>;
95
+ export type TelescopeTheme = z.infer<typeof ThemeSchema>;
96
+
97
+ // Page data structure
98
+ export interface TelescopePage {
99
+ title: string;
100
+ path: string;
101
+ description?: string;
102
+ tags?: string[];
103
+ }