specra 0.2.66 → 0.2.68
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/config/category.schema.json +58 -0
- package/dist/badges.d.ts +38 -0
- package/dist/badges.js +109 -0
- package/dist/category.d.ts +2 -0
- package/dist/changelog-context.d.ts +6 -0
- package/dist/changelog-context.js +15 -0
- package/dist/changelog.d.ts +59 -0
- package/dist/changelog.js +204 -0
- package/dist/components/docs/Changelog.svelte +71 -0
- package/dist/components/docs/Changelog.svelte.d.ts +14 -0
- package/dist/components/docs/Sidebar.svelte +3 -0
- package/dist/components/docs/Sidebar.svelte.d.ts +3 -0
- package/dist/components/docs/SidebarBadge.svelte +15 -0
- package/dist/components/docs/SidebarBadge.svelte.d.ts +7 -0
- package/dist/components/docs/SidebarMenuItems.svelte +50 -12
- package/dist/components/docs/SidebarMenuItems.svelte.d.ts +3 -0
- package/dist/components/docs/Update.svelte +75 -0
- package/dist/components/docs/Update.svelte.d.ts +19 -0
- package/dist/components/docs/index.d.ts +3 -0
- package/dist/components/docs/index.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mdx-components.d.ts +2 -2
- package/dist/mdx-components.js +4 -2
- package/dist/mdx.d.ts +5 -0
- package/dist/mdx.js +153 -17
- package/dist/rehype-changelog-ids.d.ts +20 -0
- package/dist/rehype-changelog-ids.js +43 -0
- package/dist/sidebar-utils.d.ts +3 -0
- package/dist/sidebar-utils.js +5 -1
- package/package.json +2 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { ChevronRight, ChevronDown, Lock } from 'lucide-svelte';
|
|
5
5
|
import type { SpecraConfig } from '../../config.types.js';
|
|
6
6
|
import Icon from './Icon.svelte';
|
|
7
|
+
import SidebarBadge from './SidebarBadge.svelte';
|
|
8
|
+
import { resolveBadges, type BadgeInput } from '../../badges.js';
|
|
7
9
|
import { sortSidebarItems, sortSidebarGroups } from '../../sidebar-utils.js';
|
|
8
10
|
import { renderInlineCode } from '../../inline.js';
|
|
9
11
|
|
|
@@ -21,11 +23,13 @@
|
|
|
21
23
|
categoryCollapsed?: boolean;
|
|
22
24
|
categoryIcon?: string;
|
|
23
25
|
categoryTabGroup?: string;
|
|
26
|
+
categoryBadge?: BadgeInput;
|
|
24
27
|
meta?: {
|
|
25
28
|
icon?: string;
|
|
26
29
|
tab_group?: string;
|
|
27
30
|
sidebar_position?: number;
|
|
28
31
|
order?: number;
|
|
32
|
+
badge?: BadgeInput;
|
|
29
33
|
[key: string]: any;
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
label: string;
|
|
35
39
|
path: string;
|
|
36
40
|
icon?: string;
|
|
41
|
+
badge?: BadgeInput;
|
|
37
42
|
items: DocItem[];
|
|
38
43
|
position: number;
|
|
39
44
|
collapsible: boolean;
|
|
@@ -150,6 +155,7 @@
|
|
|
150
155
|
if (isIndexFile) {
|
|
151
156
|
rootGroups[groupName].position = doc.sidebar_position ?? 999;
|
|
152
157
|
rootGroups[groupName].icon = doc.categoryIcon;
|
|
158
|
+
rootGroups[groupName].badge = doc.categoryBadge;
|
|
153
159
|
}
|
|
154
160
|
rootGroups[groupName].items.push(doc);
|
|
155
161
|
return;
|
|
@@ -168,14 +174,19 @@
|
|
|
168
174
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
169
175
|
.join(' ');
|
|
170
176
|
|
|
177
|
+
const isOwnCategory = i === folderParts.length - 1;
|
|
178
|
+
|
|
171
179
|
if (!currentLevel[folder]) {
|
|
172
180
|
currentLevel[folder] = {
|
|
173
181
|
label:
|
|
174
|
-
doc.categoryLabel &&
|
|
182
|
+
doc.categoryLabel && isOwnCategory
|
|
175
183
|
? doc.categoryLabel
|
|
176
184
|
: folderLabel,
|
|
177
185
|
path: currentPath,
|
|
178
186
|
icon: doc.categoryIcon,
|
|
187
|
+
// A doc's `_category_.json` describes its *own* folder, so the
|
|
188
|
+
// badge must not leak onto the ancestor folders we walk through.
|
|
189
|
+
badge: isOwnCategory ? doc.categoryBadge : undefined,
|
|
179
190
|
items: [],
|
|
180
191
|
position: doc.categoryPosition ?? 999,
|
|
181
192
|
collapsible: doc.categoryCollapsible ?? true,
|
|
@@ -184,7 +195,10 @@
|
|
|
184
195
|
};
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
if (
|
|
198
|
+
if (isOwnCategory) {
|
|
199
|
+
if (doc.categoryBadge) {
|
|
200
|
+
currentLevel[folder].badge = doc.categoryBadge;
|
|
201
|
+
}
|
|
188
202
|
if (isIndexFile) {
|
|
189
203
|
currentLevel[folder].position =
|
|
190
204
|
doc.categoryPosition ?? doc.sidebar_position ?? 999;
|
|
@@ -287,6 +301,7 @@
|
|
|
287
301
|
{@const marginLeft = depth > 0 ? 'ml-4' : ''}
|
|
288
302
|
{@const groupHref = getGroupHref(group)}
|
|
289
303
|
{@const mergedItems = getMergedItems(group)}
|
|
304
|
+
{@const groupBadges = resolveBadges(group.badge)}
|
|
290
305
|
|
|
291
306
|
<div class="space-y-1 {marginLeft}">
|
|
292
307
|
<div class="flex items-center group">
|
|
@@ -296,14 +311,21 @@
|
|
|
296
311
|
e.preventDefault();
|
|
297
312
|
toggleSection(groupKey);
|
|
298
313
|
}}
|
|
299
|
-
class="flex items-center gap-2 flex-1 px-3 py-2 text-sm font-semibold rounded-l-xl transition-all {isGroupActive
|
|
314
|
+
class="flex items-center gap-2 flex-1 min-w-0 px-3 py-2 text-sm font-semibold rounded-l-xl transition-all {isGroupActive
|
|
300
315
|
? 'bg-primary/10 text-primary'
|
|
301
316
|
: 'text-foreground hover:bg-accent/50'}"
|
|
302
317
|
>
|
|
303
318
|
{#if group.icon}
|
|
304
319
|
<Icon icon={group.icon} size={16} className="shrink-0" />
|
|
305
320
|
{/if}
|
|
306
|
-
{@html renderInlineCode(group.label)}
|
|
321
|
+
<span class="truncate">{@html renderInlineCode(group.label)}</span>
|
|
322
|
+
{#if groupBadges.length > 0}
|
|
323
|
+
<span class="ml-auto flex items-center gap-1 shrink-0">
|
|
324
|
+
{#each groupBadges as badge (badge.text)}
|
|
325
|
+
<SidebarBadge {badge} />
|
|
326
|
+
{/each}
|
|
327
|
+
</span>
|
|
328
|
+
{/if}
|
|
307
329
|
</a>
|
|
308
330
|
|
|
309
331
|
{#if hasContent && group.collapsible && config.navigation?.collapsibleSidebar}
|
|
@@ -333,19 +355,27 @@
|
|
|
333
355
|
{:else}
|
|
334
356
|
{@const href = `${docsBase}/${item.doc.slug}`}
|
|
335
357
|
{@const isActive = pathname === href}
|
|
358
|
+
{@const badges = resolveBadges(item.doc.meta?.badge)}
|
|
336
359
|
<a
|
|
337
360
|
{href}
|
|
338
361
|
onclick={onLinkClick}
|
|
339
|
-
class="flex items-center gap-2 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
362
|
+
class="flex items-center gap-2 min-w-0 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
340
363
|
? 'bg-primary/10 text-primary font-medium'
|
|
341
364
|
: 'text-foreground hover:text-foreground hover:bg-accent/50'}"
|
|
342
365
|
>
|
|
343
366
|
{#if item.doc.meta?.icon}
|
|
344
367
|
<Icon icon={item.doc.meta.icon} size={16} className="shrink-0" />
|
|
345
368
|
{/if}
|
|
346
|
-
{@html renderInlineCode(item.doc.title)}
|
|
347
|
-
{#if item.doc.meta?.isProtected}
|
|
348
|
-
<
|
|
369
|
+
<span class="truncate">{@html renderInlineCode(item.doc.title)}</span>
|
|
370
|
+
{#if badges.length > 0 || item.doc.meta?.isProtected}
|
|
371
|
+
<span class="ml-auto flex items-center gap-1 shrink-0">
|
|
372
|
+
{#each badges as badge (badge.text)}
|
|
373
|
+
<SidebarBadge {badge} />
|
|
374
|
+
{/each}
|
|
375
|
+
{#if item.doc.meta?.isProtected}
|
|
376
|
+
<Lock size={14} class="text-muted-foreground" />
|
|
377
|
+
{/if}
|
|
378
|
+
</span>
|
|
349
379
|
{/if}
|
|
350
380
|
</a>
|
|
351
381
|
{/if}
|
|
@@ -360,19 +390,27 @@
|
|
|
360
390
|
{#each sortedStandalone as doc (doc.slug)}
|
|
361
391
|
{@const href = `${docsBase}/${doc.slug}`}
|
|
362
392
|
{@const isActive = pathname === href}
|
|
393
|
+
{@const badges = resolveBadges(doc.meta?.badge)}
|
|
363
394
|
<a
|
|
364
395
|
{href}
|
|
365
396
|
onclick={onLinkClick}
|
|
366
|
-
class="flex items-center gap-2 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
397
|
+
class="flex items-center gap-2 min-w-0 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
367
398
|
? 'bg-primary/10 text-primary font-medium'
|
|
368
399
|
: 'text-foreground hover:text-foreground hover:bg-accent/50'}"
|
|
369
400
|
>
|
|
370
401
|
{#if doc.meta?.icon}
|
|
371
402
|
<Icon icon={doc.meta.icon} size={16} className="shrink-0" />
|
|
372
403
|
{/if}
|
|
373
|
-
{@html renderInlineCode(doc.title)}
|
|
374
|
-
{#if doc.meta?.isProtected}
|
|
375
|
-
<
|
|
404
|
+
<span class="truncate">{@html renderInlineCode(doc.title)}</span>
|
|
405
|
+
{#if badges.length > 0 || doc.meta?.isProtected}
|
|
406
|
+
<span class="ml-auto flex items-center gap-1 shrink-0">
|
|
407
|
+
{#each badges as badge (badge.text)}
|
|
408
|
+
<SidebarBadge {badge} />
|
|
409
|
+
{/each}
|
|
410
|
+
{#if doc.meta?.isProtected}
|
|
411
|
+
<Lock size={14} class="text-muted-foreground" />
|
|
412
|
+
{/if}
|
|
413
|
+
</span>
|
|
376
414
|
{/if}
|
|
377
415
|
</a>
|
|
378
416
|
{/each}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SpecraConfig } from '../../config.types.js';
|
|
2
|
+
import { type BadgeInput } from '../../badges.js';
|
|
2
3
|
interface DocItem {
|
|
3
4
|
title: string;
|
|
4
5
|
slug: string;
|
|
@@ -13,11 +14,13 @@ interface DocItem {
|
|
|
13
14
|
categoryCollapsed?: boolean;
|
|
14
15
|
categoryIcon?: string;
|
|
15
16
|
categoryTabGroup?: string;
|
|
17
|
+
categoryBadge?: BadgeInput;
|
|
16
18
|
meta?: {
|
|
17
19
|
icon?: string;
|
|
18
20
|
tab_group?: string;
|
|
19
21
|
sidebar_position?: number;
|
|
20
22
|
order?: number;
|
|
23
|
+
badge?: BadgeInput;
|
|
21
24
|
[key: string]: any;
|
|
22
25
|
};
|
|
23
26
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { getChangelogContext } from '../../changelog-context.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Date or release name. Anchors the entry and names it in the ToC. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Slug of `label`, injected server-side by rehype-changelog-ids. */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Secondary line, typically a version. */
|
|
11
|
+
description?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
/** Overrides the RSS entry. Consumed server-side; unused when rendering. */
|
|
14
|
+
rss?: { title?: string; description?: string };
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { label, id, description, tags = [], children }: Props = $props();
|
|
19
|
+
|
|
20
|
+
// Absent outside a <Changelog>, in which case nothing filters this update.
|
|
21
|
+
const changelog = getChangelogContext();
|
|
22
|
+
|
|
23
|
+
// Reads the parent's selected-tag state, so toggling a filter re-renders.
|
|
24
|
+
let visible = $derived(changelog ? changelog.matches(tags) : true);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if visible}
|
|
28
|
+
<div
|
|
29
|
+
{id}
|
|
30
|
+
class="specra-update grid gap-x-8 gap-y-3 pb-12 scroll-mt-24 last:pb-0 md:grid-cols-[10rem_minmax(0,1fr)]"
|
|
31
|
+
>
|
|
32
|
+
<div class="md:sticky md:top-24 md:self-start md:text-right">
|
|
33
|
+
{#if label}
|
|
34
|
+
{#if id}
|
|
35
|
+
<a
|
|
36
|
+
href="#{id}"
|
|
37
|
+
class="text-sm font-semibold text-foreground transition-colors hover:text-primary"
|
|
38
|
+
>
|
|
39
|
+
{label}
|
|
40
|
+
</a>
|
|
41
|
+
{:else}
|
|
42
|
+
<span class="text-sm font-semibold text-foreground">{label}</span>
|
|
43
|
+
{/if}
|
|
44
|
+
{/if}
|
|
45
|
+
|
|
46
|
+
{#if description}
|
|
47
|
+
<div class="mt-1 text-xs text-muted-foreground">{description}</div>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
{#if tags.length > 0}
|
|
51
|
+
<div class="mt-2 flex flex-wrap gap-1 md:justify-end">
|
|
52
|
+
{#each tags as tag (tag)}
|
|
53
|
+
<span
|
|
54
|
+
class="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium leading-none text-muted-foreground"
|
|
55
|
+
>
|
|
56
|
+
{tag}
|
|
57
|
+
</span>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="relative border-l border-border pl-8">
|
|
64
|
+
<span
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
class="absolute -left-[4.5px] top-2 h-2 w-2 rounded-full bg-border ring-4 ring-background"
|
|
67
|
+
></span>
|
|
68
|
+
<div class="prose prose-sm dark:prose-invert max-w-none [&>*:last-child]:mb-0">
|
|
69
|
+
{#if children}
|
|
70
|
+
{@render children()}
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Date or release name. Anchors the entry and names it in the ToC. */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Slug of `label`, injected server-side by rehype-changelog-ids. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Secondary line, typically a version. */
|
|
8
|
+
description?: string;
|
|
9
|
+
tags?: string[];
|
|
10
|
+
/** Overrides the RSS entry. Consumed server-side; unused when rendering. */
|
|
11
|
+
rss?: {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
declare const Update: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Update = ReturnType<typeof Update>;
|
|
19
|
+
export default Update;
|
|
@@ -37,7 +37,10 @@ export { default as MobileSidebarWrapper } from './MobileSidebarWrapper.svelte';
|
|
|
37
37
|
export { default as NotFoundContent } from './NotFoundContent.svelte';
|
|
38
38
|
export { default as SearchHighlight } from './SearchHighlight.svelte';
|
|
39
39
|
export { default as SearchModal } from './SearchModal.svelte';
|
|
40
|
+
export { default as Changelog } from './Changelog.svelte';
|
|
41
|
+
export { default as Update } from './Update.svelte';
|
|
40
42
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
43
|
+
export { default as SidebarBadge } from './SidebarBadge.svelte';
|
|
41
44
|
export { default as SidebarMenuItems } from './SidebarMenuItems.svelte';
|
|
42
45
|
export { default as SidebarSkeleton } from './SidebarSkeleton.svelte';
|
|
43
46
|
export { default as SiteBanner } from './SiteBanner.svelte';
|
|
@@ -38,7 +38,10 @@ export { default as MobileSidebarWrapper } from './MobileSidebarWrapper.svelte';
|
|
|
38
38
|
export { default as NotFoundContent } from './NotFoundContent.svelte';
|
|
39
39
|
export { default as SearchHighlight } from './SearchHighlight.svelte';
|
|
40
40
|
export { default as SearchModal } from './SearchModal.svelte';
|
|
41
|
+
export { default as Changelog } from './Changelog.svelte';
|
|
42
|
+
export { default as Update } from './Update.svelte';
|
|
41
43
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
44
|
+
export { default as SidebarBadge } from './SidebarBadge.svelte';
|
|
42
45
|
export { default as SidebarMenuItems } from './SidebarMenuItems.svelte';
|
|
43
46
|
export { default as SidebarSkeleton } from './SidebarSkeleton.svelte';
|
|
44
47
|
export { default as SiteBanner } from './SiteBanner.svelte';
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export * from './utils.js';
|
|
|
14
14
|
export * from './links.js';
|
|
15
15
|
export * from './inline.js';
|
|
16
16
|
export * from './sidebar-utils.js';
|
|
17
|
+
export * from './badges.js';
|
|
18
|
+
export * from './changelog.js';
|
|
19
|
+
export * from './changelog-context.js';
|
|
17
20
|
export * from './category.js';
|
|
18
21
|
export * from './redirects.js';
|
|
19
22
|
export * from './dev-utils.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ export * from './utils.js';
|
|
|
18
18
|
export * from './links.js';
|
|
19
19
|
export * from './inline.js';
|
|
20
20
|
export * from './sidebar-utils.js';
|
|
21
|
+
export * from './badges.js';
|
|
22
|
+
export * from './changelog.js';
|
|
23
|
+
export * from './changelog-context.js';
|
|
21
24
|
export * from './category.js';
|
|
22
25
|
export * from './redirects.js';
|
|
23
26
|
export * from './dev-utils.js';
|
package/dist/mdx-components.d.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
import type { Component } from 'svelte';
|
|
17
|
-
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference } from './components/docs';
|
|
18
|
-
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, };
|
|
17
|
+
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update } from './components/docs';
|
|
18
|
+
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update, };
|
|
19
19
|
/**
|
|
20
20
|
* Component map for passing to layout components that render MDX content.
|
|
21
21
|
*/
|
package/dist/mdx-components.js
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* <Callout type="info">This is a callout</Callout>
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, } from './components/docs';
|
|
16
|
+
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update, } from './components/docs';
|
|
17
17
|
// Re-export all MDX-usable components
|
|
18
|
-
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, };
|
|
18
|
+
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update, };
|
|
19
19
|
/**
|
|
20
20
|
* Component map for passing to layout components that render MDX content.
|
|
21
21
|
*/
|
|
@@ -50,4 +50,6 @@ export const mdxComponents = {
|
|
|
50
50
|
ApiResponse,
|
|
51
51
|
ApiPlayground,
|
|
52
52
|
ApiReference,
|
|
53
|
+
Changelog,
|
|
54
|
+
Update,
|
|
53
55
|
};
|
package/dist/mdx.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BadgeInput } from "./badges";
|
|
1
2
|
import type { I18nConfig } from "./config.types";
|
|
2
3
|
/**
|
|
3
4
|
* Structured node type for MDX content rendering.
|
|
@@ -12,6 +13,7 @@ export interface MdxNode {
|
|
|
12
13
|
props?: Record<string, any>;
|
|
13
14
|
children?: MdxNode[];
|
|
14
15
|
}
|
|
16
|
+
export declare function processMarkdownToMdxNodes(markdown: string): Promise<MdxNode[]>;
|
|
15
17
|
export interface DocMeta {
|
|
16
18
|
title: string;
|
|
17
19
|
description?: string;
|
|
@@ -34,6 +36,8 @@ export interface DocMeta {
|
|
|
34
36
|
word_count?: number;
|
|
35
37
|
icon?: string;
|
|
36
38
|
tab_group?: string;
|
|
39
|
+
badge?: BadgeInput;
|
|
40
|
+
rss?: boolean;
|
|
37
41
|
locale?: string;
|
|
38
42
|
protected?: boolean;
|
|
39
43
|
isProtected?: boolean;
|
|
@@ -51,6 +55,7 @@ export interface Doc {
|
|
|
51
55
|
categoryCollapsed?: boolean;
|
|
52
56
|
categoryIcon?: string;
|
|
53
57
|
categoryTabGroup?: string;
|
|
58
|
+
categoryBadge?: BadgeInput;
|
|
54
59
|
locale?: string;
|
|
55
60
|
}
|
|
56
61
|
export interface TocItem {
|
package/dist/mdx.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "path";
|
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
5
|
import { rehypeBasePath } from "./rehype-base-path.js";
|
|
6
|
+
import { rehypeChangelogIds } from "./rehype-changelog-ids.js";
|
|
7
|
+
import { annotateChangelogNodes } from "./changelog.js";
|
|
6
8
|
import { remarkCodeMeta } from "./remark-code-meta.js";
|
|
7
9
|
import { unified } from "unified";
|
|
8
10
|
import remarkParse from "remark-parse";
|
|
@@ -10,6 +12,7 @@ import remarkGfm from "remark-gfm";
|
|
|
10
12
|
import remarkMath from "remark-math";
|
|
11
13
|
import remarkRehype from "remark-rehype";
|
|
12
14
|
import rehypeSlug from "rehype-slug";
|
|
15
|
+
import GithubSlugger from "github-slugger";
|
|
13
16
|
import rehypeRaw from "rehype-raw";
|
|
14
17
|
import rehypeKatex from "rehype-katex";
|
|
15
18
|
import rehypeStringify from "rehype-stringify";
|
|
@@ -66,6 +69,8 @@ const COMPONENT_TAG_MAP = {
|
|
|
66
69
|
apiresponse: 'ApiResponse',
|
|
67
70
|
apiplayground: 'ApiPlayground',
|
|
68
71
|
apireference: 'ApiReference',
|
|
72
|
+
changelog: 'Changelog',
|
|
73
|
+
update: 'Update',
|
|
69
74
|
};
|
|
70
75
|
/**
|
|
71
76
|
* Map of lowercased attribute names to their correct camelCase form.
|
|
@@ -170,11 +175,92 @@ function maskInlineCodePipes(markdown) {
|
|
|
170
175
|
}).join('');
|
|
171
176
|
}
|
|
172
177
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
178
|
+
* Private Use Area characters used to mask `<` and `>` written inside code
|
|
179
|
+
* (inline spans or fenced blocks) that sits within a component block.
|
|
180
|
+
*
|
|
181
|
+
* Why: `ensureComponentBlockIntegrity` deliberately collapses blank lines so
|
|
182
|
+
* that an entire `<Callout>…</Callout>` stays one CommonMark HTML block. But
|
|
183
|
+
* inside an HTML block, markdown no longer applies — code spans and fences stop
|
|
184
|
+
* being code — so a documented tag such as
|
|
185
|
+
* An inline `<script src="/x.js">` tag.
|
|
186
|
+
* is handed to rehype-raw as *real HTML*. parse5 opens a raw-text `<script>`
|
|
187
|
+
* element that is never closed and swallows every following sibling, so the
|
|
188
|
+
* next heading silently disappears from the page and its ToC link 404s.
|
|
189
|
+
*
|
|
190
|
+
* Masking the angle brackets before remark sees them keeps the block inert for
|
|
191
|
+
* parse5, and `restoreCodeMarkers` puts them back after the component's
|
|
192
|
+
* children have been re-parsed as markdown. PUA chars pass through
|
|
193
|
+
* remark/rehype unchanged and are never HTML-escaped — same trick as
|
|
194
|
+
* `PIPE_MARKER` above.
|
|
195
|
+
*/
|
|
196
|
+
const LT_MARKER = '';
|
|
197
|
+
const GT_MARKER = '';
|
|
198
|
+
/**
|
|
199
|
+
* Mask `<` and `>` inside fenced code blocks and single-line inline code spans.
|
|
200
|
+
* Applied only to the interior of component blocks, where markdown code markers
|
|
201
|
+
* lose their meaning. Component tags themselves live outside code and are left
|
|
202
|
+
* alone, so `<Callout>` still parses as a component.
|
|
203
|
+
*/
|
|
204
|
+
function maskCodeAngleBrackets(markdown) {
|
|
205
|
+
const maskAngles = (s) => s.replace(/</g, LT_MARKER).replace(/>/g, GT_MARKER);
|
|
206
|
+
return splitByCodeFences(markdown).map(({ text, isCode }) => {
|
|
207
|
+
if (isCode)
|
|
208
|
+
return maskAngles(text);
|
|
209
|
+
let result = '';
|
|
210
|
+
let i = 0;
|
|
211
|
+
while (i < text.length) {
|
|
212
|
+
if (text[i] !== '`') {
|
|
213
|
+
result += text[i];
|
|
214
|
+
i++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
let openLen = 0;
|
|
218
|
+
while (i + openLen < text.length && text[i + openLen] === '`')
|
|
219
|
+
openLen++;
|
|
220
|
+
let j = i + openLen;
|
|
221
|
+
let closeIdx = -1;
|
|
222
|
+
while (j < text.length && text[j] !== '\n') {
|
|
223
|
+
if (text[j] === '`') {
|
|
224
|
+
let closeLen = 0;
|
|
225
|
+
while (j + closeLen < text.length && text[j + closeLen] === '`')
|
|
226
|
+
closeLen++;
|
|
227
|
+
if (closeLen === openLen) {
|
|
228
|
+
closeIdx = j;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
j += closeLen;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
j++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (closeIdx === -1) {
|
|
238
|
+
result += text.slice(i, i + openLen);
|
|
239
|
+
i += openLen;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const content = maskAngles(text.slice(i + openLen, closeIdx));
|
|
243
|
+
result += text.slice(i, i + openLen) + content + text.slice(closeIdx, closeIdx + openLen);
|
|
244
|
+
i = closeIdx + openLen;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}).join('');
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Restore `PIPE_MARKER` back to `|`, and `LT_MARKER`/`GT_MARKER` back to
|
|
252
|
+
* `<`/`>`, in a string. No-op if no marker is present (the common case), so
|
|
253
|
+
* cheap to call blanket.
|
|
175
254
|
*/
|
|
176
255
|
function restorePipeMarkers(s) {
|
|
177
|
-
|
|
256
|
+
let out = s;
|
|
257
|
+
if (out.indexOf(PIPE_MARKER) !== -1)
|
|
258
|
+
out = out.split(PIPE_MARKER).join('|');
|
|
259
|
+
if (out.indexOf(LT_MARKER) !== -1)
|
|
260
|
+
out = out.split(LT_MARKER).join('<');
|
|
261
|
+
if (out.indexOf(GT_MARKER) !== -1)
|
|
262
|
+
out = out.split(GT_MARKER).join('>');
|
|
263
|
+
return out;
|
|
178
264
|
}
|
|
179
265
|
/**
|
|
180
266
|
* Walk an MdxNode tree and restore `PIPE_MARKER` to `|` in every string
|
|
@@ -1168,7 +1254,14 @@ function ensureComponentBlockIntegrity(markdown) {
|
|
|
1168
1254
|
// code fences. The code fence content is raw text inside the HTML block and
|
|
1169
1255
|
// will be re-processed by processComponentChildren through the markdown
|
|
1170
1256
|
// pipeline, which restores proper code formatting.
|
|
1171
|
-
|
|
1257
|
+
// Mask `<`/`>` written inside code (inline spans and fences) BEFORE the
|
|
1258
|
+
// blank-line collapse turns this whole block into one raw HTML block.
|
|
1259
|
+
// Once it is raw HTML, markdown code markers no longer protect their
|
|
1260
|
+
// contents, and a documented tag like `<script src="...">` reaches parse5
|
|
1261
|
+
// as real HTML — a raw-text element that never closes and swallows every
|
|
1262
|
+
// sibling after the component, heading and all. Restored by
|
|
1263
|
+
// `restorePipeMarkers` once the children are re-parsed as markdown.
|
|
1264
|
+
const collapsed = maskCodeAngleBrackets(block).replace(/^\s*$/gm, '<!-- -->');
|
|
1172
1265
|
result += markdown.slice(lastIndex, blockStart) + collapsed;
|
|
1173
1266
|
lastIndex = blockEnd;
|
|
1174
1267
|
openTagRegex.lastIndex = blockEnd;
|
|
@@ -1179,7 +1272,7 @@ function ensureComponentBlockIntegrity(markdown) {
|
|
|
1179
1272
|
}
|
|
1180
1273
|
return markdown;
|
|
1181
1274
|
}
|
|
1182
|
-
async function processMarkdownToMdxNodes(markdown) {
|
|
1275
|
+
export async function processMarkdownToMdxNodes(markdown) {
|
|
1183
1276
|
// Mask pipes inside inline code spans so GFM tables containing
|
|
1184
1277
|
// `{{ x | filter }}`-style code don't get their rows broken.
|
|
1185
1278
|
const masked = maskInlineCodePipes(markdown);
|
|
@@ -1200,6 +1293,9 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1200
1293
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
1201
1294
|
.use(rehypeRaw)
|
|
1202
1295
|
.use(rehypeSlug)
|
|
1296
|
+
// After rehypeSlug so the intent is obvious: replay the heading sequence
|
|
1297
|
+
// through a second slugger, then anchor each <Update> in that namespace.
|
|
1298
|
+
.use(rehypeChangelogIds)
|
|
1203
1299
|
.use(rehypeKatex);
|
|
1204
1300
|
if (basePath) {
|
|
1205
1301
|
processor.use(rehypeBasePath, { basePath });
|
|
@@ -1210,6 +1306,10 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1210
1306
|
const children = hast.children || [];
|
|
1211
1307
|
const nodes = await hastChildrenToMdxNodes(children);
|
|
1212
1308
|
restorePipeMarkersInNodes(nodes);
|
|
1309
|
+
// Union each <Changelog>'s child tags into `allTags`. Done here, on the
|
|
1310
|
+
// server, because children render after their parent during SSR — a filter
|
|
1311
|
+
// bar that waited for its updates to self-register would serialize empty.
|
|
1312
|
+
annotateChangelogNodes(nodes);
|
|
1213
1313
|
return nodes;
|
|
1214
1314
|
}
|
|
1215
1315
|
/**
|
|
@@ -1477,6 +1577,7 @@ export function getAllDocs(version = "v1.0.0", locale, product) {
|
|
|
1477
1577
|
doc.categoryCollapsed = categoryConfig.collapsed;
|
|
1478
1578
|
doc.categoryIcon = categoryConfig.icon;
|
|
1479
1579
|
doc.categoryTabGroup = categoryConfig.tab_group;
|
|
1580
|
+
doc.categoryBadge = categoryConfig.badge;
|
|
1480
1581
|
}
|
|
1481
1582
|
}
|
|
1482
1583
|
return doc;
|
|
@@ -1621,19 +1722,54 @@ export function getAdjacentDocs(currentSlug, allDocs) {
|
|
|
1621
1722
|
};
|
|
1622
1723
|
}
|
|
1623
1724
|
export function extractTableOfContents(content) {
|
|
1624
|
-
|
|
1725
|
+
// One regex over both kinds of anchor so they come back in document order.
|
|
1726
|
+
// group 1/2 — an ATX heading, at any level (see the slugger note below)
|
|
1727
|
+
// group 3 — the `label` of an <Update>, which anchors a changelog entry
|
|
1728
|
+
// `[^>]*?` (not `.`) spans newlines, because authors wrap long <Update> tags.
|
|
1729
|
+
const anchorRegex = /^(#{1,6})[ \t]+(.+)$|<update\b[^>]*?\blabel\s*=\s*["']([^"']+)["']/gim;
|
|
1625
1730
|
const toc = [];
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1731
|
+
// Mirror rehype-slug exactly: it builds ONE GithubSlugger per document and
|
|
1732
|
+
// slugs every h1–h6 in order, so a repeated heading renders as `setup` and
|
|
1733
|
+
// then `setup-1`. github-slugger's bare `slug()` export is stateless and
|
|
1734
|
+
// cannot know that, so it emitted `setup` twice and the second ToC link
|
|
1735
|
+
// silently scrolled to the first heading.
|
|
1736
|
+
//
|
|
1737
|
+
// The slugger must see headings the ToC never displays (h1, h4–h6) because
|
|
1738
|
+
// they still consume names from the same namespace — `# Setup` followed by
|
|
1739
|
+
// `## Setup` renders the h2 as `setup-1`.
|
|
1740
|
+
//
|
|
1741
|
+
// <Update> labels share this slugger too, mirroring rehype-changelog-ids.ts,
|
|
1742
|
+
// so a label can never collide with a heading of the same text.
|
|
1743
|
+
const slugger = new GithubSlugger();
|
|
1744
|
+
// Headings inside fenced code blocks are code, not headings. They get no
|
|
1745
|
+
// `id` in the rendered page, so a ToC entry for them can only ever 404 —
|
|
1746
|
+
// and rehype-slug never sees them either, so skipping them keeps the two
|
|
1747
|
+
// sluggers in lockstep. The same reasoning covers a fenced <Update> example.
|
|
1748
|
+
for (const { text: segment, isCode } of splitByCodeFences(content)) {
|
|
1749
|
+
if (isCode)
|
|
1750
|
+
continue;
|
|
1751
|
+
let match;
|
|
1752
|
+
anchorRegex.lastIndex = 0;
|
|
1753
|
+
while ((match = anchorRegex.exec(segment)) !== null) {
|
|
1754
|
+
const [, hashes, headingText, updateLabel] = match;
|
|
1755
|
+
if (updateLabel !== undefined) {
|
|
1756
|
+
const label = updateLabel.trim();
|
|
1757
|
+
if (!label)
|
|
1758
|
+
continue;
|
|
1759
|
+
// Updates sit at the ToC's top level, alongside h2s.
|
|
1760
|
+
toc.push({ id: slugger.slug(label), title: label, level: 2 });
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const level = hashes.length;
|
|
1764
|
+
const text = headingText.trim();
|
|
1765
|
+
// Backticks are stripped first because rehype-slug slugs the RENDERED
|
|
1766
|
+
// heading text, where `code` markup is already gone. Always slug, even
|
|
1767
|
+
// for levels we drop, to keep the occurrence counters aligned.
|
|
1768
|
+
const id = slugger.slug(text.replace(/`/g, ""));
|
|
1769
|
+
if (level < 2 || level > 3)
|
|
1770
|
+
continue;
|
|
1771
|
+
toc.push({ id, title: text, level });
|
|
1772
|
+
}
|
|
1637
1773
|
}
|
|
1638
1774
|
return toc;
|
|
1639
1775
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin that gives every `<Update>` a stable anchor id.
|
|
3
|
+
*
|
|
4
|
+
* The id is slugged from the update's `label`, using a slugger that has ALSO
|
|
5
|
+
* consumed every heading on the page. rehype-slug builds one GithubSlugger per
|
|
6
|
+
* document and feeds it every h1–h6 in order; by replaying that same sequence
|
|
7
|
+
* here we land in the same namespace. Two consequences:
|
|
8
|
+
*
|
|
9
|
+
* - An `<Update label="Changelog">` on a page that also has `## Changelog`
|
|
10
|
+
* becomes `changelog-1` instead of silently duplicating the heading's id.
|
|
11
|
+
* - Two updates sharing a label get `v1` and `v1-1`, never a collision.
|
|
12
|
+
*
|
|
13
|
+
* `extractTableOfContents` performs the same walk over the raw markdown, so the
|
|
14
|
+
* anchors it links to are byte-identical to the ones rendered here.
|
|
15
|
+
*
|
|
16
|
+
* Manually walks the tree to avoid ESM/CJS issues with unist-util-visit, matching
|
|
17
|
+
* rehype-base-path.ts.
|
|
18
|
+
*/
|
|
19
|
+
import type { Root } from 'hast';
|
|
20
|
+
export declare function rehypeChangelogIds(): (tree: Root) => void;
|