@opendocsdev/cli 0.2.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.
- package/LICENSE +661 -0
- package/README.md +300 -0
- package/dist/bin/opendocs.js +712 -0
- package/dist/bin/opendocs.js.map +1 -0
- package/dist/templates/api-reference.mdx +308 -0
- package/dist/templates/components.mdx +286 -0
- package/dist/templates/configuration.mdx +190 -0
- package/dist/templates/docs.json +27 -0
- package/dist/templates/introduction.mdx +25 -0
- package/dist/templates/logo.svg +4 -0
- package/dist/templates/quickstart.mdx +59 -0
- package/dist/templates/writing-content.mdx +236 -0
- package/package.json +92 -0
- package/src/engine/astro.config.ts +75 -0
- package/src/engine/src/components/Analytics.astro +57 -0
- package/src/engine/src/components/ApiPlayground.astro +24 -0
- package/src/engine/src/components/Callout.astro +66 -0
- package/src/engine/src/components/Card.astro +75 -0
- package/src/engine/src/components/CardGroup.astro +29 -0
- package/src/engine/src/components/CodeGroup.astro +231 -0
- package/src/engine/src/components/CopyButton.astro +179 -0
- package/src/engine/src/components/Steps.astro +27 -0
- package/src/engine/src/components/Tab.astro +21 -0
- package/src/engine/src/components/TableOfContents.astro +119 -0
- package/src/engine/src/components/Tabs.astro +135 -0
- package/src/engine/src/components/index.ts +107 -0
- package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
- package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
- package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
- package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
- package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
- package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
- package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
- package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
- package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
- package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
- package/src/engine/src/components/react/Callout.tsx +54 -0
- package/src/engine/src/components/react/Card.tsx +48 -0
- package/src/engine/src/components/react/CardGroup.tsx +24 -0
- package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
- package/src/engine/src/components/react/GitHubLink.tsx +28 -0
- package/src/engine/src/components/react/NavigationCard.tsx +53 -0
- package/src/engine/src/components/react/PageActions.tsx +124 -0
- package/src/engine/src/components/react/PageFooter.tsx +91 -0
- package/src/engine/src/components/react/SearchModal.tsx +358 -0
- package/src/engine/src/components/react/SearchProvider.tsx +37 -0
- package/src/engine/src/components/react/Sidebar.tsx +369 -0
- package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
- package/src/engine/src/components/react/Steps.tsx +25 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
- package/src/engine/src/components/react/index.ts +14 -0
- package/src/engine/src/env.d.ts +10 -0
- package/src/engine/src/layouts/DocsLayout.astro +357 -0
- package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
- package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
- package/src/engine/src/lib/config.ts +79 -0
- package/src/engine/src/lib/markdown.ts +54 -0
- package/src/engine/src/lib/mdx-loader.ts +143 -0
- package/src/engine/src/lib/mdx-utils.ts +72 -0
- package/src/engine/src/lib/remark-opendocs.ts +195 -0
- package/src/engine/src/lib/utils.ts +221 -0
- package/src/engine/src/pages/[...slug].astro +115 -0
- package/src/engine/src/pages/index.astro +71 -0
- package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
- package/src/engine/src/scripts/theme-init.ts +25 -0
- package/src/engine/src/styles/global.css +703 -0
- package/src/engine/tailwind.config.mjs +60 -0
- package/src/engine/tsconfig.json +15 -0
- package/src/templates/api-reference.mdx +308 -0
- package/src/templates/components.mdx +286 -0
- package/src/templates/configuration.mdx +190 -0
- package/src/templates/docs.json +27 -0
- package/src/templates/introduction.mdx +25 -0
- package/src/templates/logo.svg +4 -0
- package/src/templates/quickstart.mdx +59 -0
- package/src/templates/writing-content.mdx +236 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* DocsLayout - Main documentation page layout
|
|
4
|
+
* Provides sidebar navigation, table of contents, and page footer
|
|
5
|
+
*/
|
|
6
|
+
import TableOfContents from "../components/TableOfContents.astro";
|
|
7
|
+
import Analytics from "../components/Analytics.astro";
|
|
8
|
+
import CopyButton from "../components/CopyButton.astro";
|
|
9
|
+
import { Sidebar } from "../components/react/Sidebar";
|
|
10
|
+
import { PageFooter } from "../components/react/PageFooter";
|
|
11
|
+
import { PageActions } from "../components/react/PageActions";
|
|
12
|
+
import { SearchProvider } from "../components/react/SearchProvider";
|
|
13
|
+
import { loadConfig } from "../lib/config.js";
|
|
14
|
+
import { generateColorVariants, getPageNavigation } from "../lib/utils";
|
|
15
|
+
import "../styles/global.css";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
headings?: { depth: number; slug: string; text: string }[];
|
|
21
|
+
path?: string;
|
|
22
|
+
lastUpdated?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { title, description, headings = [], path, lastUpdated } = Astro.props;
|
|
26
|
+
|
|
27
|
+
// Load cached config
|
|
28
|
+
const config = await loadConfig();
|
|
29
|
+
|
|
30
|
+
// Extract config values
|
|
31
|
+
const currentPath = path || Astro.url.pathname;
|
|
32
|
+
const siteName = config.name || "Documentation";
|
|
33
|
+
const faviconPath = config.favicon || "/favicon.ico";
|
|
34
|
+
const navigation = config.navigation || [];
|
|
35
|
+
const logo = config.logo;
|
|
36
|
+
const githubUrl = config.socialLinks?.github;
|
|
37
|
+
const feedbackEnabled = config.features?.feedback !== false;
|
|
38
|
+
|
|
39
|
+
// Backend config
|
|
40
|
+
const backendConfig = config.backend as { apiUrl?: string; siteId?: string } | undefined;
|
|
41
|
+
const backend = backendConfig?.apiUrl;
|
|
42
|
+
const siteId = backendConfig?.siteId;
|
|
43
|
+
|
|
44
|
+
// Theme colors
|
|
45
|
+
const primaryColor = config.theme?.primaryColor || "#3b82f6";
|
|
46
|
+
const accentColor = config.theme?.accentColor || primaryColor;
|
|
47
|
+
const primary = generateColorVariants(primaryColor);
|
|
48
|
+
const accent = generateColorVariants(accentColor);
|
|
49
|
+
|
|
50
|
+
// SEO metadata
|
|
51
|
+
const siteUrl = (config.metadata as { url?: string; ogImage?: string } | undefined)?.url || "";
|
|
52
|
+
const ogImage = (config.metadata as { url?: string; ogImage?: string } | undefined)?.ogImage;
|
|
53
|
+
const canonicalUrl = siteUrl ? `${siteUrl}${currentPath}` : "";
|
|
54
|
+
const pageDescription = description || `${title} - ${siteName}`;
|
|
55
|
+
const fullTitle = `${title} | ${siteName}`;
|
|
56
|
+
|
|
57
|
+
// Build breadcrumb segments from current path
|
|
58
|
+
const pathSegments = currentPath.split("/").filter(Boolean);
|
|
59
|
+
const breadcrumbItems = pathSegments.map((segment, i) => ({
|
|
60
|
+
name: segment.replace(/-/g, " ").replace(/\b\w/g, (c: string) => c.toUpperCase()),
|
|
61
|
+
url: `${siteUrl}/${pathSegments.slice(0, i + 1).join("/")}/`,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Page navigation (previous/next links)
|
|
65
|
+
const { previous: previousPage, next: nextPage } = getPageNavigation(navigation, currentPath);
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
<!doctype html>
|
|
69
|
+
<html lang="en">
|
|
70
|
+
<head>
|
|
71
|
+
<meta charset="UTF-8" />
|
|
72
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
73
|
+
<meta name="description" content={pageDescription} />
|
|
74
|
+
<link rel="icon" type="image/x-icon" href={faviconPath} />
|
|
75
|
+
<title>{fullTitle}</title>
|
|
76
|
+
|
|
77
|
+
{/* Canonical URL */}
|
|
78
|
+
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
|
|
79
|
+
|
|
80
|
+
{/* Theme color */}
|
|
81
|
+
<meta name="theme-color" content={primaryColor} />
|
|
82
|
+
|
|
83
|
+
{/* Open Graph */}
|
|
84
|
+
<meta property="og:type" content="article" />
|
|
85
|
+
<meta property="og:title" content={fullTitle} />
|
|
86
|
+
<meta property="og:description" content={pageDescription} />
|
|
87
|
+
<meta property="og:site_name" content={siteName} />
|
|
88
|
+
{canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
|
|
89
|
+
{ogImage && <meta property="og:image" content={ogImage.startsWith("http") ? ogImage : `${siteUrl}${ogImage}`} />}
|
|
90
|
+
|
|
91
|
+
{/* Twitter Card */}
|
|
92
|
+
<meta name="twitter:card" content={ogImage ? "summary_large_image" : "summary"} />
|
|
93
|
+
<meta name="twitter:title" content={fullTitle} />
|
|
94
|
+
<meta name="twitter:description" content={pageDescription} />
|
|
95
|
+
{ogImage && <meta name="twitter:image" content={ogImage.startsWith("http") ? ogImage : `${siteUrl}${ogImage}`} />}
|
|
96
|
+
|
|
97
|
+
{/* Structured Data */}
|
|
98
|
+
{siteUrl && (
|
|
99
|
+
<script type="application/ld+json" set:html={JSON.stringify({
|
|
100
|
+
"@context": "https://schema.org",
|
|
101
|
+
"@graph": [
|
|
102
|
+
{
|
|
103
|
+
"@type": "WebSite",
|
|
104
|
+
name: siteName,
|
|
105
|
+
url: siteUrl,
|
|
106
|
+
},
|
|
107
|
+
...(breadcrumbItems.length > 0 ? [{
|
|
108
|
+
"@type": "BreadcrumbList",
|
|
109
|
+
itemListElement: breadcrumbItems.map((item, i) => ({
|
|
110
|
+
"@type": "ListItem",
|
|
111
|
+
position: i + 1,
|
|
112
|
+
name: item.name,
|
|
113
|
+
item: item.url,
|
|
114
|
+
})),
|
|
115
|
+
}] : []),
|
|
116
|
+
],
|
|
117
|
+
})} />
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
<Analytics path={currentPath} />
|
|
121
|
+
|
|
122
|
+
<!-- Theme init script - prevents flash of wrong theme -->
|
|
123
|
+
<script is:inline>
|
|
124
|
+
(function () {
|
|
125
|
+
const storedTheme = localStorage.getItem("theme");
|
|
126
|
+
if (storedTheme === "dark") {
|
|
127
|
+
document.documentElement.classList.add("dark");
|
|
128
|
+
} else if (storedTheme === "light") {
|
|
129
|
+
document.documentElement.classList.remove("dark");
|
|
130
|
+
} else {
|
|
131
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
132
|
+
if (prefersDark) {
|
|
133
|
+
document.documentElement.classList.add("dark");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<!-- Theme injection from docs.json config -->
|
|
140
|
+
<style set:html={`
|
|
141
|
+
:root {
|
|
142
|
+
--color-primary: ${primary.base};
|
|
143
|
+
--color-primary-light: ${primary.light};
|
|
144
|
+
--color-primary-dark: ${primary.dark};
|
|
145
|
+
--color-accent: ${accent.base};
|
|
146
|
+
--color-accent-light: ${accent.light};
|
|
147
|
+
}
|
|
148
|
+
.dark {
|
|
149
|
+
--color-primary: ${primary.darkMode};
|
|
150
|
+
--color-primary-light: ${primary.lightDark};
|
|
151
|
+
--color-accent-light: ${accent.lightDark};
|
|
152
|
+
}
|
|
153
|
+
`}></style>
|
|
154
|
+
</head>
|
|
155
|
+
|
|
156
|
+
<body class="min-h-screen bg-[var(--color-background)]">
|
|
157
|
+
<div class="flex min-h-screen">
|
|
158
|
+
<!-- Desktop Sidebar -->
|
|
159
|
+
<aside class="hidden lg:block fixed inset-y-0 left-0 z-40 w-64 border-r border-[var(--color-border)] bg-[var(--color-surface-raised)]">
|
|
160
|
+
<Sidebar
|
|
161
|
+
client:load
|
|
162
|
+
siteName={siteName}
|
|
163
|
+
navigation={navigation}
|
|
164
|
+
logo={logo}
|
|
165
|
+
currentPath={currentPath}
|
|
166
|
+
githubUrl={githubUrl}
|
|
167
|
+
/>
|
|
168
|
+
</aside>
|
|
169
|
+
|
|
170
|
+
<!-- Mobile Sidebar Backdrop -->
|
|
171
|
+
<div
|
|
172
|
+
data-backdrop
|
|
173
|
+
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 lg:hidden"
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
></div>
|
|
176
|
+
|
|
177
|
+
<!-- Mobile Sidebar -->
|
|
178
|
+
<aside
|
|
179
|
+
data-mobile-sidebar
|
|
180
|
+
class="fixed inset-y-0 left-0 z-50 w-[280px] bg-[var(--color-surface-raised)] border-r border-[var(--color-border)] transform -translate-x-full transition-transform duration-300 ease-in-out lg:hidden"
|
|
181
|
+
>
|
|
182
|
+
<div class="h-full overflow-y-auto">
|
|
183
|
+
<Sidebar
|
|
184
|
+
client:load
|
|
185
|
+
siteName={siteName}
|
|
186
|
+
navigation={navigation}
|
|
187
|
+
logo={logo}
|
|
188
|
+
currentPath={currentPath}
|
|
189
|
+
githubUrl={githubUrl}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</aside>
|
|
193
|
+
|
|
194
|
+
<!-- Main content area -->
|
|
195
|
+
<div class="flex-1 lg:ml-64">
|
|
196
|
+
<!-- Mobile Header -->
|
|
197
|
+
<header class="sticky top-0 z-30 border-b border-[var(--color-border)] bg-[var(--color-background)]/95 backdrop-blur-sm lg:hidden">
|
|
198
|
+
<div class="flex items-center justify-between h-16 px-4">
|
|
199
|
+
<!-- Hamburger menu -->
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
data-hamburger
|
|
203
|
+
class="flex items-center justify-center w-11 h-11 -ml-2 text-[var(--color-muted)] hover:text-[var(--color-foreground)] rounded-md hover:bg-[var(--color-surface-raised)] transition-colors"
|
|
204
|
+
aria-label="Open navigation menu"
|
|
205
|
+
>
|
|
206
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
207
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
208
|
+
</svg>
|
|
209
|
+
</button>
|
|
210
|
+
|
|
211
|
+
<!-- Logo/site name -->
|
|
212
|
+
<a href="/" class="flex items-center justify-center">
|
|
213
|
+
{logo ? (
|
|
214
|
+
typeof logo === "string" ? (
|
|
215
|
+
<img src={logo} alt={siteName} class="h-6 w-auto" />
|
|
216
|
+
) : logo.dark ? (
|
|
217
|
+
<>
|
|
218
|
+
<img src={logo.light} alt={siteName} class="h-6 w-auto dark:hidden" />
|
|
219
|
+
<img src={logo.dark} alt={siteName} class="h-6 w-auto hidden dark:block" />
|
|
220
|
+
</>
|
|
221
|
+
) : (
|
|
222
|
+
<img src={logo.light} alt={siteName} class="h-6 w-auto" />
|
|
223
|
+
)
|
|
224
|
+
) : (
|
|
225
|
+
<span class="text-lg font-semibold text-[var(--color-foreground)]">{siteName}</span>
|
|
226
|
+
)}
|
|
227
|
+
</a>
|
|
228
|
+
|
|
229
|
+
<!-- Search button -->
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
data-search-trigger
|
|
233
|
+
class="flex items-center justify-center w-11 h-11 -mr-2 text-[var(--color-muted)] hover:text-[var(--color-foreground)] rounded-md hover:bg-[var(--color-surface-raised)] transition-colors"
|
|
234
|
+
aria-label="Search documentation"
|
|
235
|
+
>
|
|
236
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
237
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
238
|
+
</svg>
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
</header>
|
|
242
|
+
|
|
243
|
+
<!-- Main content with table of contents -->
|
|
244
|
+
<div class="flex justify-center">
|
|
245
|
+
<!-- Article content -->
|
|
246
|
+
<main class="min-w-0 flex-1 px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-6 lg:py-8 max-w-4xl">
|
|
247
|
+
<div id="page-actions-container" class="hidden">
|
|
248
|
+
<PageActions client:load githubEditUrl={githubUrl ? `${githubUrl}/edit/main${currentPath}.mdx` : undefined} />
|
|
249
|
+
</div>
|
|
250
|
+
<article class="prose prose-gray max-w-none">
|
|
251
|
+
<slot />
|
|
252
|
+
</article>
|
|
253
|
+
<PageFooter
|
|
254
|
+
client:load
|
|
255
|
+
path={currentPath}
|
|
256
|
+
backend={backend}
|
|
257
|
+
siteId={siteId}
|
|
258
|
+
feedbackEnabled={feedbackEnabled}
|
|
259
|
+
previousPage={previousPage}
|
|
260
|
+
nextPage={nextPage}
|
|
261
|
+
lastUpdated={lastUpdated}
|
|
262
|
+
/>
|
|
263
|
+
<CopyButton />
|
|
264
|
+
</main>
|
|
265
|
+
|
|
266
|
+
<!-- Table of contents -->
|
|
267
|
+
{headings.length > 0 && (
|
|
268
|
+
<aside class="hidden xl:block w-56 flex-shrink-0">
|
|
269
|
+
<div class="sticky top-8 py-8">
|
|
270
|
+
<TableOfContents headings={headings} />
|
|
271
|
+
</div>
|
|
272
|
+
</aside>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Search modal -->
|
|
279
|
+
<SearchProvider client:load backend={backend} siteId={siteId} />
|
|
280
|
+
|
|
281
|
+
<!-- Mobile sidebar toggle script -->
|
|
282
|
+
<script src="../scripts/mobile-sidebar.ts"></script>
|
|
283
|
+
|
|
284
|
+
<!-- Search trigger for mobile header -->
|
|
285
|
+
<script>
|
|
286
|
+
function initSearchTriggers() {
|
|
287
|
+
const searchTriggers = document.querySelectorAll('[data-search-trigger]');
|
|
288
|
+
searchTriggers.forEach((trigger) => {
|
|
289
|
+
trigger.addEventListener('click', () => {
|
|
290
|
+
document.dispatchEvent(new CustomEvent('open-search'));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (document.readyState === 'loading') {
|
|
296
|
+
document.addEventListener('DOMContentLoaded', initSearchTriggers);
|
|
297
|
+
} else {
|
|
298
|
+
initSearchTriggers();
|
|
299
|
+
}
|
|
300
|
+
document.addEventListener('astro:after-swap', initSearchTriggers);
|
|
301
|
+
</script>
|
|
302
|
+
|
|
303
|
+
<!-- Inject page actions after first heading -->
|
|
304
|
+
<script>
|
|
305
|
+
function injectPageActions() {
|
|
306
|
+
const container = document.getElementById('page-actions-container');
|
|
307
|
+
const article = document.querySelector('article');
|
|
308
|
+
if (!container || !article) return;
|
|
309
|
+
|
|
310
|
+
const h1 = article.querySelector('h1');
|
|
311
|
+
if (!h1) return;
|
|
312
|
+
|
|
313
|
+
let insertAfter: Element = h1;
|
|
314
|
+
const nextSibling = h1.nextElementSibling;
|
|
315
|
+
if (nextSibling && nextSibling.tagName === 'P') {
|
|
316
|
+
insertAfter = nextSibling;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
container.classList.remove('hidden');
|
|
320
|
+
container.classList.add('my-6');
|
|
321
|
+
insertAfter.after(container);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (document.readyState === 'loading') {
|
|
325
|
+
document.addEventListener('DOMContentLoaded', injectPageActions);
|
|
326
|
+
} else {
|
|
327
|
+
injectPageActions();
|
|
328
|
+
}
|
|
329
|
+
document.addEventListener('astro:after-swap', injectPageActions);
|
|
330
|
+
</script>
|
|
331
|
+
|
|
332
|
+
<!-- Wrap tables in scrollable containers -->
|
|
333
|
+
<script>
|
|
334
|
+
function wrapTables() {
|
|
335
|
+
const article = document.querySelector('article');
|
|
336
|
+
if (!article) return;
|
|
337
|
+
|
|
338
|
+
const tables = article.querySelectorAll('table:not(.table-wrapper table)');
|
|
339
|
+
tables.forEach((table) => {
|
|
340
|
+
if (table.parentElement?.classList.contains('table-wrapper')) return;
|
|
341
|
+
|
|
342
|
+
const wrapper = document.createElement('div');
|
|
343
|
+
wrapper.className = 'table-wrapper';
|
|
344
|
+
table.parentNode?.insertBefore(wrapper, table);
|
|
345
|
+
wrapper.appendChild(table);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (document.readyState === 'loading') {
|
|
350
|
+
document.addEventListener('DOMContentLoaded', wrapTables);
|
|
351
|
+
} else {
|
|
352
|
+
wrapTables();
|
|
353
|
+
}
|
|
354
|
+
document.addEventListener('astro:after-swap', wrapTables);
|
|
355
|
+
</script>
|
|
356
|
+
</body>
|
|
357
|
+
</html>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseFrontmatter, extractHeadings } from "../markdown.js";
|
|
3
|
+
|
|
4
|
+
describe("parseFrontmatter", () => {
|
|
5
|
+
it("parses frontmatter with title and description", () => {
|
|
6
|
+
const content = `---
|
|
7
|
+
title: Hello World
|
|
8
|
+
description: A test page
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Content here`;
|
|
12
|
+
|
|
13
|
+
const result = parseFrontmatter(content);
|
|
14
|
+
|
|
15
|
+
expect(result.frontmatter.title).toBe("Hello World");
|
|
16
|
+
expect(result.frontmatter.description).toBe("A test page");
|
|
17
|
+
expect(result.body).toBe("# Content here");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("handles quoted values in frontmatter", () => {
|
|
21
|
+
const content = `---
|
|
22
|
+
title: "Hello World"
|
|
23
|
+
description: 'A test page'
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Content`;
|
|
27
|
+
|
|
28
|
+
const result = parseFrontmatter(content);
|
|
29
|
+
|
|
30
|
+
expect(result.frontmatter.title).toBe("Hello World");
|
|
31
|
+
expect(result.frontmatter.description).toBe("A test page");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns empty frontmatter when none exists", () => {
|
|
35
|
+
const content = "# Just a heading\n\nSome content";
|
|
36
|
+
|
|
37
|
+
const result = parseFrontmatter(content);
|
|
38
|
+
|
|
39
|
+
expect(result.frontmatter).toEqual({});
|
|
40
|
+
expect(result.body).toBe(content);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles empty frontmatter", () => {
|
|
44
|
+
const content = `---
|
|
45
|
+
---
|
|
46
|
+
# Content`;
|
|
47
|
+
|
|
48
|
+
const result = parseFrontmatter(content);
|
|
49
|
+
|
|
50
|
+
expect(result.frontmatter).toEqual({});
|
|
51
|
+
expect(result.body).toBe("# Content");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("extractHeadings", () => {
|
|
56
|
+
it("extracts h2 and h3 headings", () => {
|
|
57
|
+
const content = `# Title (ignored)
|
|
58
|
+
|
|
59
|
+
## Getting Started
|
|
60
|
+
|
|
61
|
+
Some text
|
|
62
|
+
|
|
63
|
+
### Installation
|
|
64
|
+
|
|
65
|
+
More text
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
### Options`;
|
|
70
|
+
|
|
71
|
+
const headings = extractHeadings(content);
|
|
72
|
+
|
|
73
|
+
expect(headings).toHaveLength(4);
|
|
74
|
+
expect(headings[0]).toEqual({
|
|
75
|
+
depth: 2,
|
|
76
|
+
text: "Getting Started",
|
|
77
|
+
slug: "getting-started",
|
|
78
|
+
});
|
|
79
|
+
expect(headings[1]).toEqual({
|
|
80
|
+
depth: 3,
|
|
81
|
+
text: "Installation",
|
|
82
|
+
slug: "installation",
|
|
83
|
+
});
|
|
84
|
+
expect(headings[2]).toEqual({
|
|
85
|
+
depth: 2,
|
|
86
|
+
text: "Configuration",
|
|
87
|
+
slug: "configuration",
|
|
88
|
+
});
|
|
89
|
+
expect(headings[3]).toEqual({ depth: 3, text: "Options", slug: "options" });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("generates slugs correctly", () => {
|
|
93
|
+
const content = `## Hello World!
|
|
94
|
+
## API Reference (v2)
|
|
95
|
+
## Some--weird---slug`;
|
|
96
|
+
|
|
97
|
+
const headings = extractHeadings(content);
|
|
98
|
+
|
|
99
|
+
expect(headings[0].slug).toBe("hello-world");
|
|
100
|
+
expect(headings[1].slug).toBe("api-reference-v2");
|
|
101
|
+
expect(headings[2].slug).toBe("some-weird-slug");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns empty array for content without headings", () => {
|
|
105
|
+
const content = "Just some text without any headings";
|
|
106
|
+
|
|
107
|
+
const headings = extractHeadings(content);
|
|
108
|
+
|
|
109
|
+
expect(headings).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("extracts h4, h5, h6 headings", () => {
|
|
113
|
+
const content = `#### Level 4
|
|
114
|
+
##### Level 5
|
|
115
|
+
###### Level 6`;
|
|
116
|
+
|
|
117
|
+
const headings = extractHeadings(content);
|
|
118
|
+
|
|
119
|
+
expect(headings).toHaveLength(3);
|
|
120
|
+
expect(headings[0].depth).toBe(4);
|
|
121
|
+
expect(headings[1].depth).toBe(5);
|
|
122
|
+
expect(headings[2].depth).toBe(6);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isSnippetPath,
|
|
4
|
+
extractSlugFromPath,
|
|
5
|
+
filterPathsToSlugs,
|
|
6
|
+
} from "../mdx-utils.js";
|
|
7
|
+
|
|
8
|
+
describe("isSnippetPath", () => {
|
|
9
|
+
it("returns true for paths containing /snippets/", () => {
|
|
10
|
+
expect(isSnippetPath("path/to/snippets/file.mdx", ["snippets"])).toBe(true);
|
|
11
|
+
expect(isSnippetPath("/snippets/simple-text.mdx", ["snippets"])).toBe(true);
|
|
12
|
+
expect(isSnippetPath("docs/snippets/test.mdx", ["snippets"])).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns true for paths starting with snippet folder", () => {
|
|
16
|
+
expect(isSnippetPath("snippets/file.mdx", ["snippets"])).toBe(true);
|
|
17
|
+
expect(isSnippetPath("snippets/nested/file.mdx", ["snippets"])).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns false for non-snippet paths", () => {
|
|
21
|
+
expect(isSnippetPath("docs/introduction.mdx", ["snippets"])).toBe(false);
|
|
22
|
+
expect(isSnippetPath("guides/getting-started.mdx", ["snippets"])).toBe(
|
|
23
|
+
false
|
|
24
|
+
);
|
|
25
|
+
expect(isSnippetPath("api/reference.mdx", ["snippets"])).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns true for custom snippet folders", () => {
|
|
29
|
+
expect(isSnippetPath("path/to/shared/file.mdx", ["shared"])).toBe(true);
|
|
30
|
+
expect(isSnippetPath("partials/header.mdx", ["partials"])).toBe(true);
|
|
31
|
+
expect(isSnippetPath("includes/footer.mdx", ["includes"])).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns true for multiple custom snippet folders", () => {
|
|
35
|
+
const folders = ["snippets", "shared", "partials"];
|
|
36
|
+
expect(isSnippetPath("snippets/test.mdx", folders)).toBe(true);
|
|
37
|
+
expect(isSnippetPath("shared/test.mdx", folders)).toBe(true);
|
|
38
|
+
expect(isSnippetPath("partials/test.mdx", folders)).toBe(true);
|
|
39
|
+
expect(isSnippetPath("docs/test.mdx", folders)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("does not match partial folder names", () => {
|
|
43
|
+
expect(isSnippetPath("snippetsextra/file.mdx", ["snippets"])).toBe(false);
|
|
44
|
+
expect(isSnippetPath("mysnippets/file.mdx", ["snippets"])).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("extractSlugFromPath", () => {
|
|
49
|
+
describe("with @content/ prefix", () => {
|
|
50
|
+
it("returns null for paths in /snippets/ folder", () => {
|
|
51
|
+
expect(
|
|
52
|
+
extractSlugFromPath("@content/snippets/simple-text.mdx", ["snippets"])
|
|
53
|
+
).toBeNull();
|
|
54
|
+
expect(
|
|
55
|
+
extractSlugFromPath("@content/snippets/nested/file.mdx", ["snippets"])
|
|
56
|
+
).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns null for custom snippet folder paths", () => {
|
|
60
|
+
expect(
|
|
61
|
+
extractSlugFromPath("@content/shared/component.mdx", ["shared"])
|
|
62
|
+
).toBeNull();
|
|
63
|
+
expect(
|
|
64
|
+
extractSlugFromPath("@content/partials/header.mdx", ["partials"])
|
|
65
|
+
).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns valid slug for non-snippet paths", () => {
|
|
69
|
+
expect(
|
|
70
|
+
extractSlugFromPath("@content/introduction.mdx", ["snippets"])
|
|
71
|
+
).toBe("introduction");
|
|
72
|
+
expect(
|
|
73
|
+
extractSlugFromPath("@content/guides/getting-started.mdx", ["snippets"])
|
|
74
|
+
).toBe("guides/getting-started");
|
|
75
|
+
expect(
|
|
76
|
+
extractSlugFromPath("@content/api/reference.md", ["snippets"])
|
|
77
|
+
).toBe("api/reference");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("with relative paths (Vite glob format)", () => {
|
|
82
|
+
it("returns null for relative paths containing snippet folder", () => {
|
|
83
|
+
expect(
|
|
84
|
+
extractSlugFromPath(
|
|
85
|
+
"../../../../test-docs/snippets/file.mdx",
|
|
86
|
+
["snippets"]
|
|
87
|
+
)
|
|
88
|
+
).toBeNull();
|
|
89
|
+
expect(
|
|
90
|
+
extractSlugFromPath("../../../project/snippets/test.mdx", ["snippets"])
|
|
91
|
+
).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns null for custom snippet folder in relative paths", () => {
|
|
95
|
+
expect(
|
|
96
|
+
extractSlugFromPath("../../../../test-docs/shared/file.mdx", ["shared"])
|
|
97
|
+
).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("extracts slug from relative paths preserving directory structure", () => {
|
|
101
|
+
expect(
|
|
102
|
+
extractSlugFromPath(
|
|
103
|
+
"../../../../test-docs/guides/using-snippets.mdx",
|
|
104
|
+
["snippets"]
|
|
105
|
+
)
|
|
106
|
+
).toBe("guides/using-snippets");
|
|
107
|
+
expect(
|
|
108
|
+
extractSlugFromPath("../../../../test-docs/introduction.mdx", [
|
|
109
|
+
"snippets",
|
|
110
|
+
])
|
|
111
|
+
).toBe("introduction");
|
|
112
|
+
expect(
|
|
113
|
+
extractSlugFromPath(
|
|
114
|
+
"../../../../test-docs/api/v2/reference.mdx",
|
|
115
|
+
["snippets"]
|
|
116
|
+
)
|
|
117
|
+
).toBe("api/v2/reference");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("fallback behavior", () => {
|
|
122
|
+
it("extracts slug from paths with absolute-like format", () => {
|
|
123
|
+
expect(extractSlugFromPath("/docs/introduction.mdx", ["snippets"])).toBe(
|
|
124
|
+
"introduction"
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("removes extension for simple paths", () => {
|
|
129
|
+
expect(extractSlugFromPath("simple.mdx", ["snippets"])).toBe("simple");
|
|
130
|
+
expect(extractSlugFromPath("simple.md", ["snippets"])).toBe("simple");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("filterPathsToSlugs", () => {
|
|
136
|
+
it("excludes all snippet folder paths from result", () => {
|
|
137
|
+
const paths = [
|
|
138
|
+
"@content/introduction.mdx",
|
|
139
|
+
"@content/snippets/simple-text.mdx",
|
|
140
|
+
"@content/guides/getting-started.mdx",
|
|
141
|
+
"@content/snippets/with-props.mdx",
|
|
142
|
+
"@content/api/reference.mdx",
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const slugs = filterPathsToSlugs(paths, ["snippets"]);
|
|
146
|
+
|
|
147
|
+
expect(slugs).toEqual([
|
|
148
|
+
"introduction",
|
|
149
|
+
"guides/getting-started",
|
|
150
|
+
"api/reference",
|
|
151
|
+
]);
|
|
152
|
+
expect(slugs).not.toContain("snippets/simple-text");
|
|
153
|
+
expect(slugs).not.toContain("snippets/with-props");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("excludes multiple custom snippet folders", () => {
|
|
157
|
+
const paths = [
|
|
158
|
+
"@content/docs/intro.mdx",
|
|
159
|
+
"@content/snippets/a.mdx",
|
|
160
|
+
"@content/shared/b.mdx",
|
|
161
|
+
"@content/partials/c.mdx",
|
|
162
|
+
"@content/guides/setup.mdx",
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const slugs = filterPathsToSlugs(paths, ["snippets", "shared", "partials"]);
|
|
166
|
+
|
|
167
|
+
expect(slugs).toEqual(["docs/intro", "guides/setup"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("handles relative paths and excludes snippet folders", () => {
|
|
171
|
+
const paths = [
|
|
172
|
+
"../../../../test-docs/introduction.mdx",
|
|
173
|
+
"../../../../test-docs/snippets/test.mdx",
|
|
174
|
+
"../../../../test-docs/guides/usage.mdx",
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const slugs = filterPathsToSlugs(paths, ["snippets"]);
|
|
178
|
+
|
|
179
|
+
expect(slugs).toEqual(["introduction", "guides/usage"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("avoids duplicate slugs", () => {
|
|
183
|
+
const paths = [
|
|
184
|
+
"@content/intro.mdx",
|
|
185
|
+
"@content/intro.md",
|
|
186
|
+
"@content/guide.mdx",
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const slugs = filterPathsToSlugs(paths, ["snippets"]);
|
|
190
|
+
|
|
191
|
+
expect(slugs).toEqual(["intro", "guide"]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("returns empty array when all paths are snippets", () => {
|
|
195
|
+
const paths = [
|
|
196
|
+
"@content/snippets/a.mdx",
|
|
197
|
+
"@content/snippets/b.mdx",
|
|
198
|
+
"@content/snippets/c.mdx",
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const slugs = filterPathsToSlugs(paths, ["snippets"]);
|
|
202
|
+
|
|
203
|
+
expect(slugs).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
});
|