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.
- package/README.md +15 -0
- package/index.ts +28 -0
- package/package.json +54 -0
- package/src/env.d.ts +4 -0
- package/src/libs/integration.ts +82 -0
- package/src/libs/modal.ts +43 -0
- package/src/libs/telescope-element.ts +73 -0
- package/src/libs/telescope-search.ts +1103 -0
- package/src/libs/url.ts +125 -0
- package/src/libs/vite.ts +19 -0
- package/src/pages/pages.json.ts +72 -0
- package/src/schemas/config.ts +103 -0
- package/src/styles/telescope.css +662 -0
package/src/libs/url.ts
ADDED
|
@@ -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
|
+
}
|
package/src/libs/vite.ts
ADDED
|
@@ -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
|
+
}
|