@clidey/dory 0.10.0 → 0.12.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/package.json +1 -1
- package/scripts/generate-frontmatter-json.js +154 -0
- package/src/components/store.ts +67 -2
- package/src/mdx/mermaid.tsx +19 -2
- package/src/plugins/frontmatter-dev-server.ts +158 -0
- package/src/plugins/frontmatter-generator.ts +157 -0
- package/src/plugins/sanitize.ts +43 -0
- package/src/ui/layout.tsx +13 -6
- package/vite.config.ts +6 -1
package/package.json
CHANGED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
11
|
+
function getNavigationOrder(docsDir) {
|
|
12
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
15
|
+
const order = [];
|
|
16
|
+
|
|
17
|
+
function walkPages(pages) {
|
|
18
|
+
for (const page of pages) {
|
|
19
|
+
if (typeof page === 'string') {
|
|
20
|
+
order.push(page);
|
|
21
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
22
|
+
walkPages(page.pages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
28
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
29
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
30
|
+
for (const group of tab.groups) {
|
|
31
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
32
|
+
walkPages(group.pages);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return order;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function to parse frontmatter from MDX content
|
|
42
|
+
function parseFrontmatter(content) {
|
|
43
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
44
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
45
|
+
|
|
46
|
+
const frontmatter = frontmatterMatch[1];
|
|
47
|
+
const data = {};
|
|
48
|
+
|
|
49
|
+
frontmatter.split('\n').forEach(line => {
|
|
50
|
+
const [key, ...valueParts] = line.split(':');
|
|
51
|
+
if (key && valueParts.length) {
|
|
52
|
+
const value = valueParts.join(':').trim();
|
|
53
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Convert filename to path format used by the store
|
|
61
|
+
function pathFromFilename(filename, docsDir) {
|
|
62
|
+
return filename
|
|
63
|
+
.replace(docsDir, '')
|
|
64
|
+
.replace(/\/?index\.mdx$/, '/')
|
|
65
|
+
.replace(/\.mdx$/, '')
|
|
66
|
+
.replace(/\\/g, '/')
|
|
67
|
+
.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
71
|
+
function findMdxFilesMap(dir, baseDir) {
|
|
72
|
+
const files = {};
|
|
73
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
const fullPath = path.join(dir, item.name);
|
|
77
|
+
if (item.isDirectory()) {
|
|
78
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
79
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
80
|
+
// Key: relative path without .mdx, with forward slashes
|
|
81
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
82
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
83
|
+
files[rel] = fullPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Main function to generate frontmatter JSON
|
|
91
|
+
async function generateFrontmatterJson() {
|
|
92
|
+
const docsDir = path.join(__dirname, '..', 'docs');
|
|
93
|
+
const outputDir = path.join(__dirname, '..', 'dist');
|
|
94
|
+
|
|
95
|
+
// Ensure output directory exists
|
|
96
|
+
if (!fs.existsSync(outputDir)) {
|
|
97
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
101
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
102
|
+
const usedKeys = new Set();
|
|
103
|
+
const frontmatterArray = [];
|
|
104
|
+
|
|
105
|
+
// Process files in navigation order first
|
|
106
|
+
for (const navKey of navOrder) {
|
|
107
|
+
// Try both with and without "index"
|
|
108
|
+
let fileKey = navKey;
|
|
109
|
+
if (mdxFilesMap[fileKey]) {
|
|
110
|
+
usedKeys.add(fileKey);
|
|
111
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
112
|
+
fileKey = `${fileKey}/index`;
|
|
113
|
+
usedKeys.add(fileKey);
|
|
114
|
+
} else {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const filePath = mdxFilesMap[fileKey];
|
|
119
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
120
|
+
const { data } = parseFrontmatter(rawContent);
|
|
121
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
122
|
+
|
|
123
|
+
frontmatterArray.push({
|
|
124
|
+
...data,
|
|
125
|
+
path: pathname
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Process any extra files not in navigation order, sorted
|
|
130
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
131
|
+
for (const fileKey of extraKeys) {
|
|
132
|
+
const filePath = mdxFilesMap[fileKey];
|
|
133
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
134
|
+
const { data } = parseFrontmatter(rawContent);
|
|
135
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
136
|
+
|
|
137
|
+
frontmatterArray.push({
|
|
138
|
+
...data,
|
|
139
|
+
path: pathname
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Write to output file
|
|
144
|
+
const jsonContent = JSON.stringify(frontmatterArray, null, 2);
|
|
145
|
+
const outputPath = path.join(outputDir, 'frontmatter.json');
|
|
146
|
+
fs.writeFileSync(outputPath, jsonContent);
|
|
147
|
+
|
|
148
|
+
console.log(`✅ Generated frontmatter JSON file: ${outputPath}`);
|
|
149
|
+
console.log(`📄 Processed ${frontmatterArray.length} MDX files`);
|
|
150
|
+
console.log(`📝 Total JSON size: ${jsonContent.length} characters`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Run the script
|
|
154
|
+
generateFrontmatterJson().catch(console.error);
|
package/src/components/store.ts
CHANGED
|
@@ -75,11 +75,70 @@ function parseFrontMatter(content: string) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export let completeFrontMatter: Record<string, any>[] = [];
|
|
78
|
+
let preloadedFrontMatter: Record<string, any>[] | null = null;
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
+
// Preload frontmatter from JSON file (optimized approach)
|
|
81
|
+
export async function preloadFrontmatter() {
|
|
82
|
+
if (preloadedFrontMatter) return; // Already loaded
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch('/frontmatter.json');
|
|
86
|
+
if (response.ok) {
|
|
87
|
+
preloadedFrontMatter = await response.json();
|
|
88
|
+
if (preloadedFrontMatter) {
|
|
89
|
+
completeFrontMatter = [...preloadedFrontMatter];
|
|
90
|
+
|
|
91
|
+
// Update navigation titles from preloaded data
|
|
92
|
+
for (const fm of preloadedFrontMatter) {
|
|
93
|
+
updateNavigationTitle(fm.path, fm.title);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add to search index
|
|
97
|
+
await addPreloadedContentToSearch();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn('Failed to load prebuilt frontmatter JSON, falling back to dynamic loading:', error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add preloaded content to search index
|
|
106
|
+
async function addPreloadedContentToSearch() {
|
|
107
|
+
if (!preloadedFrontMatter) return;
|
|
108
|
+
|
|
109
|
+
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
|
|
110
|
+
const fileEntries = Object.entries(mdxFiles);
|
|
111
|
+
|
|
112
|
+
await Promise.all(
|
|
113
|
+
preloadedFrontMatter.map(async (fm) => {
|
|
114
|
+
const fileEntry = fileEntries.find(([filePath]) => pathFromFilename(filePath) === fm.path);
|
|
115
|
+
if (fileEntry) {
|
|
116
|
+
const [, loader] = fileEntry;
|
|
117
|
+
const rawContent = (await loader()).default;
|
|
118
|
+
const { content } = parseFrontMatter(rawContent);
|
|
119
|
+
|
|
120
|
+
searchIndex.add({
|
|
121
|
+
url: fm.path,
|
|
122
|
+
title: fm.title || '',
|
|
123
|
+
content,
|
|
124
|
+
pageTitle: fm.title || ''
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Load frontmatter for a specific pathname (optimized with fallback)
|
|
80
132
|
export async function loadMDXFrontMatterForPath(pathname: string) {
|
|
133
|
+
// If we have preloaded data, check if this path is already loaded
|
|
134
|
+
if (preloadedFrontMatter && preloadedFrontMatter.find(fm => fm.path === pathname)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if already loaded in completeFrontMatter
|
|
81
139
|
if (completeFrontMatter.find(fm => fm.path === pathname)) return;
|
|
82
140
|
|
|
141
|
+
// Fallback to dynamic loading
|
|
83
142
|
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
|
|
84
143
|
const fileEntries = Object.entries(mdxFiles);
|
|
85
144
|
|
|
@@ -103,8 +162,14 @@ export async function loadMDXFrontMatterForPath(pathname: string) {
|
|
|
103
162
|
completeFrontMatter = [...completeFrontMatter, frontmatterItem];
|
|
104
163
|
}
|
|
105
164
|
|
|
106
|
-
// Load all remaining frontmatter (after initial load)
|
|
165
|
+
// Load all remaining frontmatter (after initial load) - optimized with fallback
|
|
107
166
|
export async function loadAllMDXFrontMatter(pathname: string) {
|
|
167
|
+
// If we have preloaded data, we're already done
|
|
168
|
+
if (preloadedFrontMatter) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fallback to dynamic loading
|
|
108
173
|
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
|
|
109
174
|
const fileEntries = Object.entries(mdxFiles);
|
|
110
175
|
|
package/src/mdx/mermaid.tsx
CHANGED
|
@@ -2,6 +2,20 @@ import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'preact/compat'
|
|
4
4
|
|
|
5
|
+
function fixNodeLabelsWithSpecialChars(content: string): string {
|
|
6
|
+
// Pattern to match node definitions with brackets containing special characters
|
|
7
|
+
// Matches: NodeId[text content] where text content has special chars and isn't already quoted
|
|
8
|
+
const nodePattern = /(\w+)\[([^\]"]*[(),\s][^\]"]*)\]/g;
|
|
9
|
+
|
|
10
|
+
return content.replace(nodePattern, (match, nodeId, labelContent) => {
|
|
11
|
+
// Only wrap in quotes if not already quoted and contains special characters
|
|
12
|
+
if (!labelContent.startsWith('"') || !labelContent.endsWith('"')) {
|
|
13
|
+
return `${nodeId}["${labelContent}"]`;
|
|
14
|
+
}
|
|
15
|
+
return match;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export function MermaidRenderer({ content }: { content: string }) {
|
|
6
20
|
const mermaidRef = useRef<HTMLDivElement>(null)
|
|
7
21
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
@@ -47,9 +61,12 @@ export function MermaidRenderer({ content }: { content: string }) {
|
|
|
47
61
|
.replace(/^\s*classDef .*$/gm, '')
|
|
48
62
|
.replace(/^\s*class .*$/gm, '');
|
|
49
63
|
|
|
64
|
+
// Fix node labels with special characters by wrapping them in quotes
|
|
65
|
+
const processedContent = fixNodeLabelsWithSpecialChars(contentWithoutStyle);
|
|
66
|
+
|
|
50
67
|
// Validate the mermaid syntax first
|
|
51
68
|
try {
|
|
52
|
-
await mermaid.parse(
|
|
69
|
+
await mermaid.parse(processedContent)
|
|
53
70
|
setIsValid(true)
|
|
54
71
|
} catch (parseError) {
|
|
55
72
|
setIsValid(false)
|
|
@@ -59,7 +76,7 @@ export function MermaidRenderer({ content }: { content: string }) {
|
|
|
59
76
|
return
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
const { svg } = await mermaid.render(diagramId,
|
|
79
|
+
const { svg } = await mermaid.render(diagramId, processedContent);
|
|
63
80
|
|
|
64
81
|
if (mounted && mermaidRef.current) {
|
|
65
82
|
mermaidRef.current.innerHTML = svg
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
6
|
+
function getNavigationOrder(docsDir: string): string[] {
|
|
7
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
8
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
9
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
10
|
+
const order: string[] = [];
|
|
11
|
+
|
|
12
|
+
function walkPages(pages: any[]) {
|
|
13
|
+
for (const page of pages) {
|
|
14
|
+
if (typeof page === 'string') {
|
|
15
|
+
order.push(page);
|
|
16
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
17
|
+
walkPages(page.pages);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
23
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
24
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
25
|
+
for (const group of tab.groups) {
|
|
26
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
27
|
+
walkPages(group.pages);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return order;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Function to parse frontmatter from MDX content
|
|
37
|
+
function parseFrontmatter(content: string) {
|
|
38
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
39
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
40
|
+
|
|
41
|
+
const frontmatter = frontmatterMatch[1];
|
|
42
|
+
const data: Record<string, any> = {};
|
|
43
|
+
|
|
44
|
+
frontmatter.split('\n').forEach(line => {
|
|
45
|
+
const [key, ...valueParts] = line.split(':');
|
|
46
|
+
if (key && valueParts.length) {
|
|
47
|
+
const value = valueParts.join(':').trim();
|
|
48
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Convert filename to path format used by the store
|
|
56
|
+
function pathFromFilename(filename: string, docsDir: string): string {
|
|
57
|
+
return filename
|
|
58
|
+
.replace(docsDir, '')
|
|
59
|
+
.replace(/\/?index\.mdx$/, '/')
|
|
60
|
+
.replace(/\.mdx$/, '')
|
|
61
|
+
.replace(/\\/g, '/')
|
|
62
|
+
.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
66
|
+
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
67
|
+
const files: Record<string, string> = {};
|
|
68
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
69
|
+
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
const fullPath = path.join(dir, item.name);
|
|
72
|
+
if (item.isDirectory()) {
|
|
73
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
74
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
75
|
+
// Key: relative path without .mdx, with forward slashes
|
|
76
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
77
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
78
|
+
files[rel] = fullPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Function to generate frontmatter JSON in dory.json order, then append any extra files
|
|
86
|
+
function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
87
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
88
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
89
|
+
const usedKeys = new Set<string>();
|
|
90
|
+
const frontmatterArray: Array<Record<string, any>> = [];
|
|
91
|
+
|
|
92
|
+
// Process files in navigation order first
|
|
93
|
+
for (const navKey of navOrder) {
|
|
94
|
+
// Try both with and without "index"
|
|
95
|
+
let fileKey = navKey;
|
|
96
|
+
if (mdxFilesMap[fileKey]) {
|
|
97
|
+
usedKeys.add(fileKey);
|
|
98
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
99
|
+
fileKey = `${fileKey}/index`;
|
|
100
|
+
usedKeys.add(fileKey);
|
|
101
|
+
} else {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const filePath = mdxFilesMap[fileKey];
|
|
106
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
107
|
+
const { data } = parseFrontmatter(rawContent);
|
|
108
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
109
|
+
|
|
110
|
+
frontmatterArray.push({
|
|
111
|
+
...data,
|
|
112
|
+
path: pathname
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Process any extra files not in navigation order, sorted
|
|
117
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
118
|
+
for (const fileKey of extraKeys) {
|
|
119
|
+
const filePath = mdxFilesMap[fileKey];
|
|
120
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
const { data } = parseFrontmatter(rawContent);
|
|
122
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
123
|
+
|
|
124
|
+
frontmatterArray.push({
|
|
125
|
+
...data,
|
|
126
|
+
path: pathname
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return frontmatterArray;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function frontmatterDevServer(): Plugin {
|
|
134
|
+
return {
|
|
135
|
+
name: 'frontmatter-dev-server',
|
|
136
|
+
configureServer(server) {
|
|
137
|
+
server.middlewares.use('/frontmatter.json', (req, res, next) => {
|
|
138
|
+
if (req.method !== 'GET') {
|
|
139
|
+
return next();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
144
|
+
const frontmatterArray = generateFrontmatterJson(docsDir);
|
|
145
|
+
const jsonContent = JSON.stringify(frontmatterArray, null, 2);
|
|
146
|
+
|
|
147
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
148
|
+
res.setHeader('Content-Length', Buffer.byteLength(jsonContent));
|
|
149
|
+
res.end(jsonContent);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error generating frontmatter JSON:', error);
|
|
152
|
+
res.statusCode = 500;
|
|
153
|
+
res.end('Internal Server Error');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
interface FrontmatterGeneratorOptions {
|
|
6
|
+
docsDir?: string;
|
|
7
|
+
outputPath?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
11
|
+
function getNavigationOrder(docsDir: string): string[] {
|
|
12
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
15
|
+
const order: string[] = [];
|
|
16
|
+
|
|
17
|
+
function walkPages(pages: any[]) {
|
|
18
|
+
for (const page of pages) {
|
|
19
|
+
if (typeof page === 'string') {
|
|
20
|
+
order.push(page);
|
|
21
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
22
|
+
walkPages(page.pages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
28
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
29
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
30
|
+
for (const group of tab.groups) {
|
|
31
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
32
|
+
walkPages(group.pages);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return order;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function to parse frontmatter from MDX content
|
|
42
|
+
function parseFrontmatter(content: string) {
|
|
43
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
44
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
45
|
+
|
|
46
|
+
const frontmatter = frontmatterMatch[1];
|
|
47
|
+
const data: Record<string, any> = {};
|
|
48
|
+
|
|
49
|
+
frontmatter.split('\n').forEach(line => {
|
|
50
|
+
const [key, ...valueParts] = line.split(':');
|
|
51
|
+
if (key && valueParts.length) {
|
|
52
|
+
const value = valueParts.join(':').trim();
|
|
53
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Convert filename to path format used by the store
|
|
61
|
+
function pathFromFilename(filename: string, docsDir: string): string {
|
|
62
|
+
return filename
|
|
63
|
+
.replace(docsDir, '')
|
|
64
|
+
.replace(/\/?index\.mdx$/, '/')
|
|
65
|
+
.replace(/\.mdx$/, '')
|
|
66
|
+
.replace(/\\/g, '/')
|
|
67
|
+
.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
71
|
+
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
72
|
+
const files: Record<string, string> = {};
|
|
73
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
const fullPath = path.join(dir, item.name);
|
|
77
|
+
if (item.isDirectory()) {
|
|
78
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
79
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
80
|
+
// Key: relative path without .mdx, with forward slashes
|
|
81
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
82
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
83
|
+
files[rel] = fullPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Function to generate frontmatter JSON in dory.json order, then append any extra files
|
|
91
|
+
function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
92
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
93
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
94
|
+
const usedKeys = new Set<string>();
|
|
95
|
+
const frontmatterArray: Array<Record<string, any>> = [];
|
|
96
|
+
|
|
97
|
+
// Process files in navigation order first
|
|
98
|
+
for (const navKey of navOrder) {
|
|
99
|
+
// Try both with and without "index"
|
|
100
|
+
let fileKey = navKey;
|
|
101
|
+
if (mdxFilesMap[fileKey]) {
|
|
102
|
+
usedKeys.add(fileKey);
|
|
103
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
104
|
+
fileKey = `${fileKey}/index`;
|
|
105
|
+
usedKeys.add(fileKey);
|
|
106
|
+
} else {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const filePath = mdxFilesMap[fileKey];
|
|
111
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
112
|
+
const { data } = parseFrontmatter(rawContent);
|
|
113
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
114
|
+
|
|
115
|
+
frontmatterArray.push({
|
|
116
|
+
...data,
|
|
117
|
+
path: pathname
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Process any extra files not in navigation order, sorted
|
|
122
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
123
|
+
for (const fileKey of extraKeys) {
|
|
124
|
+
const filePath = mdxFilesMap[fileKey];
|
|
125
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
126
|
+
const { data } = parseFrontmatter(rawContent);
|
|
127
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
128
|
+
|
|
129
|
+
frontmatterArray.push({
|
|
130
|
+
...data,
|
|
131
|
+
path: pathname
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return frontmatterArray;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function frontmatterGenerator(options: FrontmatterGeneratorOptions = {}): Plugin {
|
|
139
|
+
return {
|
|
140
|
+
name: 'frontmatter-generator',
|
|
141
|
+
generateBundle() {
|
|
142
|
+
const docsDir = options.docsDir || path.join(process.cwd(), 'docs');
|
|
143
|
+
const frontmatterArray = generateFrontmatterJson(docsDir);
|
|
144
|
+
const jsonContent = JSON.stringify(frontmatterArray, null, 2);
|
|
145
|
+
|
|
146
|
+
this.emitFile({
|
|
147
|
+
type: 'asset',
|
|
148
|
+
fileName: 'frontmatter.json',
|
|
149
|
+
source: jsonContent
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log(`✅ Generated frontmatter JSON file`);
|
|
153
|
+
console.log(`📄 Processed ${frontmatterArray.length} MDX files`);
|
|
154
|
+
console.log(`📝 Total JSON size: ${jsonContent.length} characters`);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/plugins/sanitize.ts
CHANGED
|
@@ -13,3 +13,46 @@ export const remarkSafeVars = () => {
|
|
|
13
13
|
});
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
+
|
|
17
|
+
const KNOWN_COMPONENTS = [
|
|
18
|
+
// Text components
|
|
19
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'Row', 'Col', 'a', 'img',
|
|
20
|
+
// Code components
|
|
21
|
+
'code', 'CodeGroup', 'pre',
|
|
22
|
+
// UI components
|
|
23
|
+
'Accordion', 'AccordionGroup', 'Icon', 'Card', 'CardGroup',
|
|
24
|
+
'Callout', 'Note', 'Warning', 'Info', 'Tip', 'Check',
|
|
25
|
+
'Latex', 'API', 'Expandable', 'ResponseField', 'Properties', 'Property',
|
|
26
|
+
'Steps', 'Step', 'table', 'th', 'td', 'ul', 'ol', 'li',
|
|
27
|
+
'APIPlayground', 'WebSocketPlayground', 'AsyncAPI', 'Source',
|
|
28
|
+
// Common HTML elements that should be preserved
|
|
29
|
+
'div', 'span', 'br', 'hr', 'strong', 'em', 'b', 'i', 'u', 's',
|
|
30
|
+
'blockquote', 'nav', 'section', 'article', 'header', 'footer', 'main',
|
|
31
|
+
'aside', 'figure', 'figcaption', 'details', 'summary', 'mark', 'small',
|
|
32
|
+
'del', 'ins', 'sub', 'sup', 'kbd', 'samp', 'var', 'time', 'abbr',
|
|
33
|
+
'address', 'cite', 'q', 'dfn'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function preprocessMdxTags() {
|
|
37
|
+
return {
|
|
38
|
+
name: 'preprocess-mdx-tags',
|
|
39
|
+
enforce: 'pre' as const,
|
|
40
|
+
transform(code: string, id: string) {
|
|
41
|
+
if (!id.endsWith('.mdx')) return;
|
|
42
|
+
|
|
43
|
+
// Remove leading whitespace from code fence blocks
|
|
44
|
+
let processed = code.replace(/^(\s+)(```\w*)/gm, '$2');
|
|
45
|
+
|
|
46
|
+
// Replace unrecognized <someTag> with `"<someTag>"`
|
|
47
|
+
processed = processed.replace(/<([a-z][a-z0-9]+)>/gi, (match, tag) => {
|
|
48
|
+
const known = KNOWN_COMPONENTS.includes(tag);
|
|
49
|
+
return known ? match : `\`<${tag}>\``; // wrap in backticks to keep as code
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
code: processed,
|
|
54
|
+
map: null,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/ui/layout.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { ComponentChildren } from 'preact';
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
|
3
3
|
import { useLocation } from 'wouter-preact';
|
|
4
4
|
import { Navigation } from '../components/navigation';
|
|
5
|
-
import { ALL_NAVIGATION, ALL_OPENAPI,
|
|
5
|
+
import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, ALL_PAGES, pathFromFilename, preloadFrontmatter, ALL_ASYNCAPI } from '../components/store';
|
|
6
6
|
import { OpenAPI } from '../mdx/open-api';
|
|
7
7
|
import { AsyncAPI } from '../mdx/async-api';
|
|
8
8
|
import { Header } from './header';
|
|
@@ -64,14 +64,21 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
64
64
|
], [handleOpenMDX, handleCopyMDX, handleOpenChatGPT, handleOpenAnthropic]);
|
|
65
65
|
|
|
66
66
|
useEffect(() => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// Try to preload frontmatter first (optimized approach)
|
|
68
|
+
preloadFrontmatter().then(() => {
|
|
69
|
+
// If preloading succeeds, we're done
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}).catch(() => {
|
|
72
|
+
// Fallback to the original approach
|
|
73
|
+
loadMDXFrontMatterForPath(pathname).then(() => {
|
|
74
|
+
loadAllMDXFrontMatter(pathname).then(() => {
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}).catch(() => {
|
|
77
|
+
setLoading(false);
|
|
78
|
+
});
|
|
70
79
|
}).catch(() => {
|
|
71
80
|
setLoading(false);
|
|
72
81
|
});
|
|
73
|
-
}).catch(() => {
|
|
74
|
-
setLoading(false);
|
|
75
82
|
});
|
|
76
83
|
}, [pathname]);
|
|
77
84
|
|
package/vite.config.ts
CHANGED
|
@@ -3,12 +3,15 @@ import preact from '@preact/preset-vite';
|
|
|
3
3
|
import mdx from '@mdx-js/rollup';
|
|
4
4
|
import tailwindcss from '@tailwindcss/vite'
|
|
5
5
|
import remarkGfm from 'remark-gfm';
|
|
6
|
-
import { remarkSafeVars } from './src/plugins/sanitize';
|
|
6
|
+
import { preprocessMdxTags, remarkSafeVars } from './src/plugins/sanitize';
|
|
7
7
|
import { llmTxtGenerator } from './src/plugins/llm-txt-generator';
|
|
8
8
|
import { llmTxtDevServer } from './src/plugins/llm-txt-dev-server';
|
|
9
|
+
import { frontmatterGenerator } from './src/plugins/frontmatter-generator';
|
|
10
|
+
import { frontmatterDevServer } from './src/plugins/frontmatter-dev-server';
|
|
9
11
|
|
|
10
12
|
export default defineConfig({
|
|
11
13
|
plugins: [
|
|
14
|
+
preprocessMdxTags(),
|
|
12
15
|
preact(),
|
|
13
16
|
mdx({
|
|
14
17
|
providerImportSource: '@mdx-js/preact',
|
|
@@ -17,6 +20,8 @@ export default defineConfig({
|
|
|
17
20
|
tailwindcss(),
|
|
18
21
|
llmTxtGenerator(),
|
|
19
22
|
llmTxtDevServer(),
|
|
23
|
+
frontmatterGenerator(),
|
|
24
|
+
frontmatterDevServer(),
|
|
20
25
|
],
|
|
21
26
|
resolve: {
|
|
22
27
|
alias: {
|