@astrojs/starlight 0.41.10 → 0.41.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.41.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4167](https://github.com/withastro/starlight/pull/4167) [`32a5e29`](https://github.com/withastro/starlight/commit/32a5e29127c13946ce746c36d7cb194f35d11952) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes a layout issue and anchor links appearing for hidden headings, e.g. footnote headings, when `markdown.headingLinks` is enabled or the `<AnchorHeading>` component is used.
8
+
9
+ - [#4148](https://github.com/withastro/starlight/pull/4148) [`cdfafd8`](https://github.com/withastro/starlight/commit/cdfafd8eedbbd6653cd6a1d3b1542b44ed1ba19e) Thanks [@ematipico](https://github.com/ematipico)! - Optimizes sidebar data generation logic to speed up sites with large sidebars
10
+
3
11
  ## 0.41.10
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.41.10",
3
+ "version": "0.41.11",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -53,6 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@astrojs/markdown-remark": "^7.2.2",
55
55
  "@playwright/test": "^1.61.1",
56
+ "@codspeed/vitest-plugin": "^5.7.1",
56
57
  "@types/node": "^22.19.17",
57
58
  "@vitest/coverage-v8": "^4.1.5",
58
59
  "astro": "^7.1.6",
@@ -96,6 +97,8 @@
96
97
  "scripts": {
97
98
  "test": "VITE_CONFIG_NATIVE_IGNORE_WARNING=true vitest",
98
99
  "test:coverage": "VITE_CONFIG_NATIVE_IGNORE_WARNING=true vitest run --coverage",
100
+ "bench": "pnpm bench:functional",
101
+ "bench:functional": "VITE_CONFIG_NATIVE_IGNORE_WARNING=true vitest bench --run --config vitest.functional.bench.config.ts __bench_fn__/*.bench.ts",
99
102
  "test:e2e": "playwright install --with-deps chromium && playwright test",
100
103
  "test:e2e:ci": "playwright test"
101
104
  }
@@ -45,6 +45,11 @@ how we do it.
45
45
  line-height: var(--sl-line-height-headings);
46
46
  }
47
47
 
48
+ /* If the heading is hidden, remove the wrapper's box to avoid unwanted spacing. */
49
+ .sl-markdown-content .sl-heading-wrapper:has(> :first-child:is(.sr-only, [hidden])) {
50
+ display: contents;
51
+ }
52
+
48
53
  /* We need to apply the same rule we use for heading spacing to the parent wrapper. */
49
54
  .sl-markdown-content
50
55
  :not(h1, h2, h3, h4, h5, h6, .sl-heading-wrapper)
@@ -75,7 +80,7 @@ how we do it.
75
80
  /* ======================================================
76
81
  HEADING
77
82
  ====================================================== */
78
- .sl-markdown-content .sl-heading-wrapper > :first-child {
83
+ .sl-markdown-content .sl-heading-wrapper > :first-child:not([hidden]) {
79
84
  display: inline;
80
85
  /* Apply end-of-line padding to the heading element. */
81
86
  padding-inline-end: var(--sl-anchor-icon-space);
@@ -96,6 +101,13 @@ how we do it.
96
101
  display: inline-flex;
97
102
  }
98
103
 
104
+ /* Hide anchor links for headings that are hidden. */
105
+ .sl-markdown-content
106
+ .sl-heading-wrapper:has(> :first-child:is(.sr-only, [hidden]))
107
+ > .sl-anchor-link {
108
+ display: none;
109
+ }
110
+
99
111
  /* Increase clickable area for anchor links with a pseudo element that doesn’t impact layout. */
100
112
  .sl-markdown-content .sl-anchor-link::after {
101
113
  content: '';
@@ -20,7 +20,7 @@ import {
20
20
  stripExtension,
21
21
  stripLeadingAndTrailingSlashes,
22
22
  } from './path';
23
- import { getLocaleRoutes, routes } from './routing';
23
+ import { getLocaleRoutes, getRouteById } from './routing';
24
24
  import type {
25
25
  SidebarGroup,
26
26
  SidebarLink,
@@ -42,6 +42,7 @@ const SlugKey = Symbol('SlugKey');
42
42
  const rootAutogenerate: SidebarAutogenerateRouteData = { directory: '' };
43
43
 
44
44
  const neverPathFormatter = createPathFormatter({ trailingSlash: 'never' });
45
+ const sidebarCollator = new Intl.Collator(localeToLang(undefined));
45
46
 
46
47
  const docsCollectionPathFromRoot = getCollectionPathFromRoot('docs', project);
47
48
 
@@ -142,7 +143,7 @@ function linkFromInternalSidebarLinkItem(
142
143
  // Astro passes root `index.[md|mdx]` entries with a slug of `index`
143
144
  const slug = item.slug === 'index' ? '' : item.slug;
144
145
  const localizedSlug = locale ? (slug ? locale + '/' + slug : locale) : slug;
145
- const route = routes.find((entry) => localizedSlug === entry.id);
146
+ const route = getRouteById(localizedSlug);
146
147
  if (!route) {
147
148
  const hasExternalSlashes = item.slug.at(0) === '/' || item.slug.at(-1) === '/';
148
149
  if (hasExternalSlashes) {
@@ -206,11 +207,6 @@ function makeLink({ attrs = {}, badge, autogenerate, ...opts }: MakeLinkOptions)
206
207
  };
207
208
  }
208
209
 
209
- /** Test if two paths are equivalent even if formatted differently. */
210
- function pathsMatch(pathA: string, pathB: string) {
211
- return neverPathFormatter(pathA) === neverPathFormatter(pathB);
212
- }
213
-
214
210
  /** Get the segments leading to a page. */
215
211
  function getBreadcrumbs(path: string, baseDir: string): string[] {
216
212
  // Strip extension from path.
@@ -294,21 +290,26 @@ function linkFromRoute(
294
290
  * Directories have the weight of the lowest weighted route they contain.
295
291
  */
296
292
  function getOrder(routeOrDir: Route | Dir): number {
297
- return isDir(routeOrDir)
293
+ const cachedOrder = orderByEntry.get(routeOrDir);
294
+ if (cachedOrder !== undefined) return cachedOrder;
295
+ const order = isDir(routeOrDir)
298
296
  ? Math.min(...Object.values(routeOrDir).flatMap(getOrder))
299
297
  : // If no order value is found, set it to the largest number possible.
300
298
  (routeOrDir.entry.data.sidebar.order ?? Number.MAX_VALUE);
299
+ orderByEntry.set(routeOrDir, order);
300
+ return order;
301
301
  }
302
302
 
303
+ const orderByEntry = new WeakMap<Dir | Route, number>();
304
+
303
305
  /** Sort a directory’s entries by user-specified order or alphabetically if no order specified. */
304
306
  function sortDirEntries(dir: [string, Dir | Route][]): [string, Dir | Route][] {
305
- const collator = new Intl.Collator(localeToLang(undefined));
306
307
  return dir.sort(([_keyA, a], [_keyB, b]) => {
307
308
  const [aOrder, bOrder] = [getOrder(a), getOrder(b)];
308
309
  // Pages are sorted by order in ascending order.
309
310
  if (aOrder !== bOrder) return aOrder < bOrder ? -1 : 1;
310
311
  // If two pages have the same order value they will be sorted by their slug.
311
- return collator.compare(isDir(a) ? a[SlugKey] : a.id, isDir(b) ? b[SlugKey] : b.id);
312
+ return sidebarCollator.compare(isDir(a) ? a[SlugKey] : a.id, isDir(b) ? b[SlugKey] : b.id);
312
313
  });
313
314
  }
314
315
 
@@ -435,19 +436,20 @@ function setIntermediateSidebarCurrentEntry(
435
436
 
436
437
  /** Finds the current page in a sidebar. */
437
438
  function getSidebarCurrentEntry(sidebar: SidebarEntry[], pathname: string): SidebarLink | null {
438
- for (const entry of sidebar) {
439
- if (entry.type === 'link' && pathsMatch(encodeURI(entry.href), pathname)) {
440
- return entry;
441
- }
442
-
443
- if (entry.type === 'group') {
444
- const currentEntry = getSidebarCurrentEntry(entry.entries, pathname);
445
- if (currentEntry) return currentEntry;
439
+ let entriesByPath = sidebarEntriesByPath.get(sidebar);
440
+ if (!entriesByPath) {
441
+ entriesByPath = new Map();
442
+ for (const entry of flattenSidebar(sidebar)) {
443
+ const path = neverPathFormatter(encodeURI(entry.href));
444
+ if (!entriesByPath.has(path)) entriesByPath.set(path, entry);
446
445
  }
446
+ sidebarEntriesByPath.set(sidebar, entriesByPath);
447
447
  }
448
- return null;
448
+ return entriesByPath.get(neverPathFormatter(pathname)) ?? null;
449
449
  }
450
450
 
451
+ const sidebarEntriesByPath = new WeakMap<SidebarEntry[], Map<string, SidebarLink>>();
452
+
451
453
  /** Generates a deterministic string based on the content of the passed sidebar. */
452
454
  export function getSidebarHash(sidebar: SidebarEntry[]): string {
453
455
  let hash = 0;
@@ -472,11 +474,17 @@ function recursivelyBuildSidebarIdentity(sidebar: SidebarEntry[]): string {
472
474
 
473
475
  /** Turn the nested tree structure of a sidebar into a flat list of all the links. */
474
476
  export function flattenSidebar(sidebar: SidebarEntry[]): SidebarLink[] {
475
- return sidebar.flatMap((entry) =>
477
+ const cachedSidebar = flattenedSidebars.get(sidebar);
478
+ if (cachedSidebar) return cachedSidebar;
479
+ const flattenedSidebar = sidebar.flatMap((entry) =>
476
480
  entry.type === 'group' ? flattenSidebar(entry.entries) : entry
477
481
  );
482
+ flattenedSidebars.set(sidebar, flattenedSidebar);
483
+ return flattenedSidebar;
478
484
  }
479
485
 
486
+ const flattenedSidebars = new WeakMap<SidebarEntry[], SidebarLink[]>();
487
+
480
488
  /** Get previous/next pages in the sidebar or the ones from the frontmatter if any. */
481
489
  export function getPrevNextLinks(
482
490
  sidebar: SidebarEntry[],
@@ -12,7 +12,6 @@ validateLogoImports();
12
12
 
13
13
  interface Path extends GetStaticPathsItem {
14
14
  params: { slug: string | undefined };
15
- props: Route;
16
15
  }
17
16
 
18
17
  /**
@@ -61,10 +60,10 @@ function getRoutes(): Route[] {
61
60
  if (!localeConfig) continue;
62
61
  const locale = key === 'root' ? undefined : key;
63
62
  const localeDocs = getLocaleDocs(locale);
63
+ const localeDocIds = new Set(localeDocs.map(({ id }) => id));
64
64
  for (const fallback of defaultLocaleDocs) {
65
65
  const id = localizedSlug(fallback.id, locale);
66
- const doesNotNeedFallback = localeDocs.some((doc) => doc.id === id);
67
- if (doesNotNeedFallback) continue;
66
+ if (localeDocIds.has(id)) continue;
68
67
  routes.push({
69
68
  entry: fallback,
70
69
  id,
@@ -82,6 +81,12 @@ function getRoutes(): Route[] {
82
81
  }
83
82
  export const routes = getRoutes();
84
83
 
84
+ const routesById = new Map(routes.map((route) => [route.id, route]));
85
+
86
+ export function getRouteById(id: string): Route | undefined {
87
+ return routesById.get(id);
88
+ }
89
+
85
90
  function getParamRouteMapping(): ReadonlyMap<string | undefined, Route> {
86
91
  const map = new Map<string | undefined, Route>();
87
92
  for (const route of routes) {
@@ -98,7 +103,6 @@ export function getRouteBySlugParam(slugParam: string | undefined): Route | unde
98
103
  function getPaths(): Path[] {
99
104
  return routes.map((route) => ({
100
105
  params: { slug: slugToParam(route.id) },
101
- props: route,
102
106
  }));
103
107
  }
104
108
  export const paths = getPaths();