@umami/shiso 0.61.0 → 1.0.0

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.
@@ -1,7 +1,12 @@
1
- import type { SearchResult } from '@/lib/search';
1
+ import type { SearchContext, SearchResult } from '@/lib/search';
2
2
 
3
3
  export interface SearchProvider {
4
- search(query: string, limit?: number): Promise<SearchResult[]>;
4
+ /**
5
+ * `context` describes the active version/language scope. Providers may
6
+ * ignore it; the built-in local provider uses it to keep results inside
7
+ * the scope being browsed.
8
+ */
9
+ search(query: string, limit?: number, context?: SearchContext): Promise<SearchResult[]>;
5
10
  }
6
11
 
7
12
  export type SearchProviderFactory = (
@@ -1,5 +1,5 @@
1
1
  import type { SearchRecord } from '@/lib/search';
2
- import { searchIndex } from '@/lib/search';
2
+ import { filterRecordsByScope, searchIndex } from '@/lib/search';
3
3
  import type { SearchProvider } from '@/lib/search/provider';
4
4
 
5
5
  /** Built-in provider backed by the section index generated during the build. */
@@ -7,9 +7,9 @@ export function createLocalSearchProvider(_options: Record<string, unknown> = {}
7
7
  let records: Promise<SearchRecord[]> | null = null;
8
8
 
9
9
  return {
10
- async search(query, limit) {
10
+ async search(query, limit, context) {
11
11
  records ||= import('@/lib/search-index.generated').then(module => module.SEARCH_INDEX);
12
- return searchIndex(await records, query, limit);
12
+ return searchIndex(filterRecordsByScope(await records, context), query, limit);
13
13
  },
14
14
  };
15
15
  }
package/src/lib/search.ts CHANGED
@@ -18,6 +18,34 @@ export interface SearchRecord {
18
18
  id?: string;
19
19
  /** Plain text of the section. */
20
20
  text: string;
21
+ /** Navigation scope id, absent for single-scope sites. */
22
+ scopeId?: string;
23
+ /** Language label of the owning scope, when the site defines languages. */
24
+ language?: string;
25
+ /** Version label of the owning scope, when the site defines versions. */
26
+ version?: string;
27
+ }
28
+
29
+ /** Where a search originates, so providers can stay inside the active scope. */
30
+ export interface SearchContext {
31
+ scopeId?: string;
32
+ language?: string;
33
+ version?: string;
34
+ }
35
+
36
+ /**
37
+ * Restricts records to the active scope. Records without a scope id (from
38
+ * single-scope indexes or older caches) always pass.
39
+ */
40
+ export function filterRecordsByScope(
41
+ records: SearchRecord[],
42
+ context?: SearchContext,
43
+ ): SearchRecord[] {
44
+ if (!context?.scopeId) {
45
+ return records;
46
+ }
47
+
48
+ return records.filter(record => !record.scopeId || record.scopeId === context.scopeId);
21
49
  }
22
50
 
23
51
  export interface SearchResult {
@@ -1,12 +1,21 @@
1
1
  import rawConfig from 'virtual:shiso-docs-config';
2
2
  import { resolveDocFile } from '@/lib/content';
3
- import { assertDocsConfig, normalizeDocsConfig } from '@/lib/docs-config';
4
- import { stripBase, stripDocsPrefix } from '@/lib/paths';
3
+ import {
4
+ assertDocsConfig,
5
+ getDefaultScope,
6
+ getScopeForPage,
7
+ getPageByPathname as getSitePageByPathname,
8
+ normalizeDocsSite,
9
+ } from '@/lib/docs-config';
10
+ import { getTextDirection, resolveLocale } from '@/lib/locale';
11
+ import { stripBase } from '@/lib/paths';
5
12
  import { resolveSiteModel } from '@/lib/site-model';
6
13
  import type {
7
14
  DocsConfig,
15
+ DocsScope,
8
16
  NormalizedDocsConfig,
9
17
  NormalizedDocsPage,
18
+ NormalizedDocsSite,
10
19
  RedirectRule,
11
20
  SeoConfig,
12
21
  } from '@/lib/types';
@@ -15,10 +24,29 @@ assertDocsConfig(rawConfig, 'docs.json');
15
24
 
16
25
  export const siteConfig: DocsConfig = rawConfig;
17
26
 
18
- export const docsConfig: NormalizedDocsConfig = normalizeDocsConfig(siteConfig, resolveDocFile);
27
+ /** The complete normalized site: every version/language scope. */
28
+ export const docsSite: NormalizedDocsSite = normalizeDocsSite(siteConfig, resolveDocFile);
29
+
30
+ /** The default scope's navigation, used where a single navigation is expected. */
31
+ export const docsConfig: NormalizedDocsConfig = getDefaultScope(docsSite).docs;
32
+
33
+ /** Landing page of the default scope: the site-wide "docs home" URL. */
34
+ export const docsHomeUrl = getDefaultScope(docsSite).firstPageUrl;
19
35
 
20
36
  export const siteModel = resolveSiteModel(siteConfig, docsConfig);
21
37
 
38
+ /** Scope that owns the current pathname; the default scope for unknown paths. */
39
+ export function getScopeByPathname(pathname: string): DocsScope {
40
+ const page = getPageByPathname(pathname);
41
+ return page ? getScopeForPage(docsSite, page) : getDefaultScope(docsSite);
42
+ }
43
+
44
+ /** Document language and direction for a pathname, from its scope's language. */
45
+ export function getLocaleByPathname(pathname: string): { lang: string; dir: 'ltr' | 'rtl' } {
46
+ const lang = resolveLocale(getScopeByPathname(pathname).language, siteModel.locale);
47
+ return { lang, dir: getTextDirection(lang) };
48
+ }
49
+
22
50
  export const siteName = siteModel.name;
23
51
 
24
52
  /** Trailing-slash-insensitive route key for redirect matching. */
@@ -28,8 +56,8 @@ function toRouteKey(routePath: string): string {
28
56
  }
29
57
 
30
58
  /**
31
- * Redirect rules with exact-match sources. Wildcard patterns are part of the
32
- * standard but not implemented; they are skipped with a warning.
59
+ * Redirect rules with exact-match sources. Wildcard patterns are rejected by
60
+ * the schema; this guard covers configs that bypassed validation.
33
61
  */
34
62
  export function getRedirects(): RedirectRule[] {
35
63
  return (siteConfig.redirects || []).filter(rule => {
@@ -40,7 +68,7 @@ export function getRedirects(): RedirectRule[] {
40
68
  if (/[:*]/.test(rule.source)) {
41
69
  console.warn(
42
70
  `[shiso] Redirect source "${rule.source}" uses a wildcard pattern, which is not ` +
43
- 'implemented yet — it will be skipped.',
71
+ 'supported — it will be skipped. Use an exact source path.',
44
72
  );
45
73
  return false;
46
74
  }
@@ -76,23 +104,8 @@ export function showTimestamp(frontmatterValue: unknown): boolean {
76
104
  return siteModel.showTimestamp;
77
105
  }
78
106
 
79
- export function normalizeParamSlug(slug: string): string {
80
- const cleaned = slug.replace(/^\/+|\/+$/g, '');
81
-
82
- if (!cleaned) {
83
- return 'index';
84
- }
85
-
86
- if (cleaned === 'index') {
87
- return cleaned;
88
- }
89
-
90
- return cleaned.replace(/\/index$/, '') || 'index';
91
- }
92
-
93
107
  export function getPageByPathname(pathname: string): NormalizedDocsPage | null {
94
- const slug = normalizeParamSlug(stripDocsPrefix(stripBase(pathname)));
95
- return docsConfig.pageByLookupSlug[slug] || null;
108
+ return getSitePageByPathname(docsSite, stripBase(pathname));
96
109
  }
97
110
 
98
111
  export function getPageTitle(pageTitle?: string): string {
package/src/lib/types.ts CHANGED
@@ -54,7 +54,7 @@ export interface TabItem {
54
54
 
55
55
  export interface AnchorItem {
56
56
  anchor: string;
57
- href?: string;
57
+ href: string;
58
58
  icon?: string;
59
59
  hidden?: boolean;
60
60
  target?: LinkTarget;
@@ -318,6 +318,12 @@ export interface NormalizedDocsPage {
318
318
  tabId: string;
319
319
  tabLabel: string;
320
320
  order: number;
321
+ /** Id of the navigation scope (version/language) this page belongs to. */
322
+ scopeId: string;
323
+ /** Language label of the owning scope, when the site defines languages. */
324
+ language?: string;
325
+ /** Version label of the owning scope, when the site defines versions. */
326
+ version?: string;
321
327
  /**
322
328
  * Hidden pages are still routed and prerendered, so they remain reachable by
323
329
  * URL, but are excluded from the sidebar, prev/next, the
@@ -377,10 +383,41 @@ export interface NormalizedDocsConfig {
377
383
  }
378
384
 
379
385
  export interface NormalizeOptions {
380
- /** Route prefix for docs pages. Phases 6/7 pass "/v2/docs", "/es/docs", etc. */
386
+ /** Route prefix for docs pages. Default "/docs"; "" serves docs at the root. */
381
387
  docsPrefix?: string;
382
388
  }
383
389
 
390
+ /**
391
+ * One navigation scope of a site: the ordinary navigation, a version, a
392
+ * language, or a version nested inside a language. Page references alone
393
+ * determine URLs; scopes never add their own URL prefixes.
394
+ */
395
+ export interface DocsScope {
396
+ id: string;
397
+ /** Language label, when this scope belongs to a language. */
398
+ language?: string;
399
+ /** Version label, when this scope belongs to a version. */
400
+ version?: string;
401
+ /** Hidden scopes still build, but are omitted from switchers. */
402
+ hidden?: boolean;
403
+ isDefault: boolean;
404
+ /** Landing scope of its language: the language's default version. */
405
+ isLanguageDefault: boolean;
406
+ /** URL of the scope's first visible page — the landing page for switchers. */
407
+ firstPageUrl: string;
408
+ docs: NormalizedDocsConfig;
409
+ }
410
+
411
+ /** The complete normalized site: every scope, with global page lookups. */
412
+ export interface NormalizedDocsSite {
413
+ scopes: DocsScope[];
414
+ defaultScopeId: string;
415
+ /** Every routed page across all scopes, in scope order. */
416
+ pages: NormalizedDocsPage[];
417
+ /** Exact canonical URL to page, across all scopes. */
418
+ pageByUrl: Record<string, NormalizedDocsPage>;
419
+ }
420
+
384
421
  export interface NormalizedLink extends ConfigLink {
385
422
  target: LinkTarget;
386
423
  }
package/types/search.d.ts CHANGED
@@ -6,8 +6,18 @@ export interface SearchResult {
6
6
  snippet?: string;
7
7
  }
8
8
 
9
+ /**
10
+ * Where a search originates, so providers can keep results inside the
11
+ * version/language scope being browsed. Providers may ignore it.
12
+ */
13
+ export interface SearchContext {
14
+ scopeId?: string;
15
+ language?: string;
16
+ version?: string;
17
+ }
18
+
9
19
  export interface SearchProvider {
10
- search(query: string, limit?: number): Promise<SearchResult[]>;
20
+ search(query: string, limit?: number, context?: SearchContext): Promise<SearchResult[]>;
11
21
  }
12
22
 
13
23
  export type SearchProviderFactory = (